Skip to content

Commit d020d2f

Browse files
authored
Merge pull request #721 from Extra-Chill/fix-primary-guards-716-717
Separate safe primary refresh from dangerous mutations
2 parents 8b5c35c + abd7565 commit d020d2f

9 files changed

Lines changed: 320 additions & 133 deletions
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Primary Mirror Architecture
2+
3+
## Problem
4+
5+
DMC primary checkouts currently serve three roles at once: default-branch reference, mutable working tree, and base for creating task worktrees. That makes stale reads and accidental default-branch mutations likely.
6+
7+
## Target Model
8+
9+
- A primary mirror is DMC-owned reference state for a remote default branch.
10+
- Agents do feature, release, and repair work only in managed `repo@slug` worktrees.
11+
- Mirror refresh is a narrow operation: fetch and fast-forward to the tracked remote ref.
12+
- Dangerous operations on mirrors are blocked by default and require explicit operator approval.
13+
- Read surfaces fail closed when a mirror is stale, diverged, detached, or otherwise unsafe unless the caller opts in with stale-read metadata.
14+
15+
## Migration Plan
16+
17+
1. Keep existing primary directories as the compatibility mirror path.
18+
2. Enforce tactical safety first: separate primary refresh from dangerous primary mutation, and guard stale primary reads.
19+
3. Add mirror metadata to each primary row: remote URL, tracked ref, last refresh result, freshness state, and whether local commits/dirty state block automated conversion.
20+
4. For clean/current primaries, mark them `mirror_compatible` and allow DMC to refresh them as mirrors.
21+
5. For dirty/diverged primaries, keep them read-protected and report a non-destructive remediation plan: preserve local commits in a worktree, push/open PR if needed, or archive before mirror conversion.
22+
6. After migration, create release/default-branch work via explicit managed worktrees, not the mirror.
23+
24+
## Command Shape
25+
26+
- Investigation: `workspace show <repo>` then `workspace read <repo>@<slug> ...` or `workspace read <repo> ... --allow-stale-primary` when intentionally inspecting stale local state.
27+
- Refresh: `workspace git pull <repo> --allow-primary-refresh`.
28+
- Feature work: `workspace worktree add <repo> <branch> --from=origin/<base>`.
29+
- Dangerous primary repair: `workspace git <commit|push|reset|rebase> <repo> --allow-dangerous-primary-mutation` with operator approval.
30+
31+
## Open Decisions
32+
33+
- Whether mirrors should become bare repositories or remain non-bare worktrees with stricter DMC-owned metadata.
34+
- Whether stale primary reads should stay fail-closed permanently or return warnings for privileged UI callers.
35+
- How long to keep compatibility for `--allow-primary-mutation` as a pull alias.
36+
- Whether release checkouts need a distinct lifecycle state separate from normal task worktrees.

inc/Abilities/WorkspaceAbilities.php

Lines changed: 80 additions & 58 deletions
Large diffs are not rendered by default.

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1910,11 +1910,14 @@ public function show( array $args, array $assoc_args ): void {
19101910
* default: 1048576
19111911
* ---
19121912
*
1913-
* [--offset=<line>]
1914-
* : Line number to start reading from (1-indexed).
1915-
*
1916-
* [--limit=<lines>]
1917-
* : Maximum number of lines to return.
1913+
* [--offset=<line>]
1914+
* : Line number to start reading from (1-indexed).
1915+
*
1916+
* [--limit=<lines>]
1917+
* : Maximum number of lines to return.
1918+
*
1919+
* [--allow-stale-primary]
1920+
* : Explicitly read from a stale, diverged, detached, or otherwise unsafe primary checkout.
19181921
*
19191922
* ## EXAMPLES
19201923
*
@@ -1958,6 +1961,10 @@ public function read( array $args, array $assoc_args ): void {
19581961
$input['limit'] = (int) $assoc_args['limit'];
19591962
}
19601963

1964+
if ( ! empty($assoc_args['allow-stale-primary']) ) {
1965+
$input['allow_stale_primary'] = true;
1966+
}
1967+
19611968
$result = $ability->execute($input);
19621969

19631970
if ( is_wp_error($result) ) {
@@ -1980,12 +1987,15 @@ public function read( array $args, array $assoc_args ): void {
19801987
* <repo>
19811988
* : Repository directory name.
19821989
*
1983-
* [<path>]
1984-
* : Relative directory path within the repo (defaults to root).
1985-
*
1986-
* [--format=<format>]
1987-
* : Output format.
1988-
* ---
1990+
* [<path>]
1991+
* : Relative directory path within the repo (defaults to root).
1992+
*
1993+
* [--allow-stale-primary]
1994+
* : Explicitly list a stale, diverged, detached, or otherwise unsafe primary checkout.
1995+
*
1996+
* [--format=<format>]
1997+
* : Output format.
1998+
* ---
19891999
* default: table
19902000
* options:
19912001
* - table
@@ -2025,6 +2035,10 @@ public function ls( array $args, array $assoc_args ): void {
20252035
$input['path'] = $args[1];
20262036
}
20272037

2038+
if ( ! empty($assoc_args['allow-stale-primary']) ) {
2039+
$input['allow_stale_primary'] = true;
2040+
}
2041+
20282042
$result = $ability->execute($input);
20292043

20302044
if ( is_wp_error($result) ) {
@@ -2079,15 +2093,18 @@ function ( $entry ) {
20792093
* default: 100
20802094
* ---
20812095
*
2082-
* [--context-lines=<count>]
2083-
* : Number of surrounding lines to include for each match.
2084-
* ---
2085-
* default: 0
2086-
* ---
2087-
*
2088-
* [--format=<format>]
2089-
* : Output format.
2090-
* ---
2096+
* [--context-lines=<count>]
2097+
* : Number of surrounding lines to include for each match.
2098+
* ---
2099+
* default: 0
2100+
* ---
2101+
*
2102+
* [--allow-stale-primary]
2103+
* : Explicitly grep a stale, diverged, detached, or otherwise unsafe primary checkout.
2104+
*
2105+
* [--format=<format>]
2106+
* : Output format.
2107+
* ---
20912108
* default: table
20922109
* options:
20932110
* - table
@@ -2135,6 +2152,10 @@ public function grep( array $args, array $assoc_args ): void {
21352152
$input['context_lines'] = (int) $assoc_args['context-lines'];
21362153
}
21372154

2155+
if ( ! empty($assoc_args['allow-stale-primary']) ) {
2156+
$input['allow_stale_primary'] = true;
2157+
}
2158+
21382159
$result = $ability->execute($input);
21392160
if ( is_wp_error($result) ) {
21402161
WP_CLI::error($result->get_error_message());
@@ -2601,11 +2622,17 @@ private function resolveAtFile( string $value ): string {
26012622
* : Relative path (repeatable) for add/diff operations. Named `--rel`
26022623
* to avoid colliding with WP-CLI's documented global `--path` flag.
26032624
*
2604-
* [--allow-dirty]
2605-
* : Allow pull with dirty working tree.
2606-
*
2607-
* [--allow-primary-mutation]
2608-
* : Permit mutating ops (pull/add/commit/push) on the primary checkout. Default-deny — use a worktree handle (`<repo>@<branch-slug>`) instead whenever possible.
2625+
* [--allow-dirty]
2626+
* : Allow pull with dirty working tree.
2627+
*
2628+
* [--allow-primary-refresh]
2629+
* : Permit safe primary refresh with `git pull --ff-only`.
2630+
*
2631+
* [--allow-primary-mutation]
2632+
* : Legacy alias for `--allow-primary-refresh` on `git pull`, and for non-dangerous primary file/index mutations. Does not permit primary commit, push, reset, or rebase.
2633+
*
2634+
* [--allow-dangerous-primary-mutation]
2635+
* : Permit primary commit, push, reset, or rebase. Use only for an explicitly approved primary mutation.
26092636
*
26102637
* [--remote=<remote>]
26112638
* : Remote name for pull/push (default: origin).
@@ -2687,8 +2714,11 @@ public function git( array $args, array $assoc_args ): void {
26872714

26882715
$input = array( 'name' => $repo );
26892716

2690-
// Mutating ops accept --allow-primary-mutation to operate on a primary checkout.
2691-
if ( in_array($operation, array( 'pull', 'add', 'commit', 'push', 'rebase', 'reset', 'pr-rebase' ), true) ) {
2717+
if ( 'pull' === $operation ) {
2718+
$input['allow_primary_refresh'] = ! empty($assoc_args['allow-primary-refresh']) || ! empty($assoc_args['allow-primary-mutation']);
2719+
} elseif ( in_array($operation, array( 'commit', 'push', 'rebase', 'reset', 'pr-rebase' ), true) ) {
2720+
$input['allow_dangerous_primary_mutation'] = ! empty($assoc_args['allow-dangerous-primary-mutation']);
2721+
} elseif ( in_array($operation, array( 'add' ), true) ) {
26922722
$input['allow_primary_mutation'] = ! empty($assoc_args['allow-primary-mutation']);
26932723
}
26942724

inc/Runtime/AgentsMdSections.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ private static function register_datamachine_section( string $wp ): void {
144144
- **GitHub:** `{$wp} datamachine-code github {$github_subcmds}` — list/read GitHub state, manage issues and PRs, install review flows, and comment on reviews.
145145
- **Editing inside a worktree:** any tool. Local agents on the same disk should use native file I/O and raw `git`; routing edits through workspace abilities is ceremony, not safety.
146146
- **Workspace lifecycle:** use `workspace clone` for primary checkout adoption/cloning and `workspace worktree add` for isolated branches. Use the CLI `--help` output for current flags and subcommands.
147-
- **Primary freshness:** before using a primary checkout for investigation or verification, inspect `workspace list|show|hygiene` freshness metadata. If the primary is stale, run `workspace git pull <repo> --allow-primary-mutation` or create the worktree from an explicit remote ref with `worktree add <repo> <branch> --from=origin/<base>`. Do not clone a second top-level primary for the same remote just to get fresh code.
148-
- **Primary is read-only.** Never edit `<workspace>/<repo>` (no `@slug`). Mutating ops on bare `<repo>` handles via the CLI require `--allow-primary-mutation`. The primary tracks the deployed branch — operate on a worktree.
147+
- **Primary freshness:** before using a primary checkout for investigation or verification, inspect `workspace list|show|hygiene` freshness metadata. If the primary is stale, run `workspace git pull <repo> --allow-primary-refresh` or create the worktree from an explicit remote ref with `worktree add <repo> <branch> --from=origin/<base>`. Stale primary reads require an explicit `--allow-stale-primary` opt-in. Do not clone a second top-level primary for the same remote just to get fresh code.
148+
- **Primary is read-only.** Never edit `<workspace>/<repo>` (no `@slug`). Safe primary refresh uses `--allow-primary-refresh`; primary commit, push, reset, and rebase require the stronger `--allow-dangerous-primary-mutation` approval. The primary tracks the deployed branch — operate on a worktree.
149149
- **Rule:** Never modify files under `wp-content/plugins/` or `wp-content/themes/` directly. Those paths are **read-only reference**. All code changes go through the workspace so they are tracked in git and reviewed via pull requests.
150150
151151
**System:** `{$wp} datamachine system health|prompts|run` — site health, prompt inspection, diagnostic runs.

0 commit comments

Comments
 (0)