Skip to content

Commit 094bacc

Browse files
fix: separate safe primary refresh from mutations
1 parent f5b51fb commit 094bacc

9 files changed

Lines changed: 278 additions & 95 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: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,10 @@ private function registerAbilities(): void {
258258
'type' => 'integer',
259259
'description' => 'Maximum number of lines to return.',
260260
),
261+
'allow_stale_primary' => array(
262+
'type' => 'boolean',
263+
'description' => 'Explicitly allow reading from a stale, diverged, detached, or otherwise unsafe primary checkout. Worktree reads are unaffected.',
264+
),
261265
),
262266
'required' => array( 'path' ),
263267
),
@@ -295,6 +299,10 @@ private function registerAbilities(): void {
295299
'type' => 'string',
296300
'description' => 'Relative directory path within the repo (omit for root).',
297301
),
302+
'allow_stale_primary' => array(
303+
'type' => 'boolean',
304+
'description' => 'Explicitly allow listing a stale, diverged, detached, or otherwise unsafe primary checkout. Worktree reads are unaffected.',
305+
),
298306
),
299307
'required' => array(),
300308
),
@@ -356,6 +364,10 @@ private function registerAbilities(): void {
356364
'type' => 'integer',
357365
'description' => 'Number of surrounding lines to include for each match (default 0, max 10).',
358366
),
367+
'allow_stale_primary' => array(
368+
'type' => 'boolean',
369+
'description' => 'Explicitly allow grepping a stale, diverged, detached, or otherwise unsafe primary checkout. Worktree reads are unaffected.',
370+
),
359371
),
360372
'required' => array( 'pattern' ),
361373
),
@@ -824,7 +836,7 @@ private function registerAbilities(): void {
824836
'datamachine-code/workspace-git-pull',
825837
array(
826838
'label' => 'Workspace Git Pull',
827-
'description' => 'Run git pull --ff-only for a workspace handle. Mutating ops on the primary checkout require allow_primary_mutation=true.',
839+
'description' => 'Run git pull --ff-only for a workspace handle. Primary refresh requires allow_primary_refresh=true; worktrees are always allowed.',
828840
'category' => 'datamachine-code-workspace',
829841
'input_schema' => array(
830842
'type' => 'object',
@@ -837,9 +849,13 @@ private function registerAbilities(): void {
837849
'type' => 'boolean',
838850
'description' => 'Allow pull when working tree is dirty.',
839851
),
852+
'allow_primary_refresh' => array(
853+
'type' => 'boolean',
854+
'description' => 'Permit safe primary refresh with git pull --ff-only. Worktrees are always allowed.',
855+
),
840856
'allow_primary_mutation' => array(
841857
'type' => 'boolean',
842-
'description' => 'Permit mutation on the primary checkout (default false). Worktrees are always allowed.',
858+
'description' => 'Legacy alias for allow_primary_refresh on git pull only.',
843859
),
844860
'remote' => array(
845861
'type' => 'string',
@@ -965,7 +981,7 @@ private function registerAbilities(): void {
965981
'datamachine-code/workspace-git-commit',
966982
array(
967983
'label' => 'Workspace Git Commit',
968-
'description' => 'Commit staged changes in a workspace handle. Mutating ops on the primary checkout require allow_primary_mutation=true.',
984+
'description' => 'Commit staged changes in a workspace handle. Primary commits require allow_dangerous_primary_mutation=true; use a worktree whenever possible.',
969985
'category' => 'datamachine-code-workspace',
970986
'input_schema' => array(
971987
'type' => 'object',
@@ -978,9 +994,9 @@ private function registerAbilities(): void {
978994
'type' => 'string',
979995
'description' => 'Commit message.',
980996
),
981-
'allow_primary_mutation' => array(
997+
'allow_dangerous_primary_mutation' => array(
982998
'type' => 'boolean',
983-
'description' => 'Permit mutation on the primary checkout (default false). Worktrees are always allowed.',
999+
'description' => 'Permit committing on a primary checkout. Use only for an explicitly approved primary mutation.',
9841000
),
9851001
),
9861002
'required' => array( 'name', 'message' ),
@@ -1021,9 +1037,9 @@ private function registerAbilities(): void {
10211037
'type' => 'string',
10221038
'description' => 'Branch override.',
10231039
),
1024-
'allow_primary_mutation' => array(
1040+
'allow_dangerous_primary_mutation' => array(
10251041
'type' => 'boolean',
1026-
'description' => 'Permit pushing from the primary checkout (default false). Worktrees are always allowed.',
1042+
'description' => 'Permit pushing from a primary checkout. Use only for an explicitly approved primary mutation.',
10271043
),
10281044
'force_with_lease' => array(
10291045
'type' => 'boolean',
@@ -1119,9 +1135,9 @@ private function registerAbilities(): void {
11191135
'type' => 'boolean',
11201136
'description' => 'Continue an in-progress rebase after conflicts were resolved and staged.',
11211137
),
1122-
'allow_primary_mutation' => array(
1138+
'allow_dangerous_primary_mutation' => array(
11231139
'type' => 'boolean',
1124-
'description' => 'Permit mutation on a primary checkout. Default false.',
1140+
'description' => 'Permit rebasing a primary checkout. Use only for an explicitly approved primary mutation.',
11251141
),
11261142
),
11271143
'required' => array( 'name' ),
@@ -1159,9 +1175,9 @@ private function registerAbilities(): void {
11591175
'type' => 'boolean',
11601176
'description' => 'Required for hard reset.',
11611177
),
1162-
'allow_primary_mutation' => array(
1178+
'allow_dangerous_primary_mutation' => array(
11631179
'type' => 'boolean',
1164-
'description' => 'Permit mutation on a primary checkout. Default false.',
1180+
'description' => 'Permit resetting a primary checkout. Use only for an explicitly approved primary mutation.',
11651181
),
11661182
),
11671183
'required' => array( 'name' ),
@@ -1242,9 +1258,9 @@ private function registerAbilities(): void {
12421258
'items' => array( 'type' => 'string' ),
12431259
'description' => 'Glob patterns to resolve by taking the base version during rebase conflicts.',
12441260
),
1245-
'allow_primary_mutation' => array(
1261+
'allow_dangerous_primary_mutation' => array(
12461262
'type' => 'boolean',
1247-
'description' => 'Permit mutation on a primary checkout. Default false.',
1263+
'description' => 'Permit rebasing and force-with-lease pushing from a primary checkout. Use only for an explicitly approved primary mutation.',
12481264
),
12491265
),
12501266
'required' => array( 'name' ),
@@ -2568,7 +2584,8 @@ public static function readFile( array $input ): array|\WP_Error {
25682584
$input['path'] ?? '',
25692585
isset($input['max_size']) ? (int) $input['max_size'] : Workspace::MAX_READ_SIZE,
25702586
isset($input['offset']) ? (int) $input['offset'] : null,
2571-
isset($input['limit']) ? (int) $input['limit'] : null
2587+
isset($input['limit']) ? (int) $input['limit'] : null,
2588+
! empty($input['allow_stale_primary'])
25722589
);
25732590
}
25742591

@@ -2590,7 +2607,8 @@ public static function readFile( array $input ): array|\WP_Error {
25902607
$input['path'] ?? '',
25912608
isset($input['max_size']) ? (int) $input['max_size'] : Workspace::MAX_READ_SIZE,
25922609
isset($input['offset']) ? (int) $input['offset'] : null,
2593-
isset($input['limit']) ? (int) $input['limit'] : null
2610+
isset($input['limit']) ? (int) $input['limit'] : null,
2611+
! empty($input['allow_stale_primary'])
25942612
);
25952613
}
25962614

@@ -2607,7 +2625,8 @@ public static function listDirectory( array $input ): array|\WP_Error {
26072625
if ( RemoteWorkspaceBackend::should_handle() && null !== self::showLocalWorkspaceHandleIfPresent($workspace, (string) ( $input['repo'] ?? '' )) ) {
26082626
return $reader->list_directory(
26092627
$input['repo'] ?? '',
2610-
$input['path'] ?? null
2628+
$input['path'] ?? null,
2629+
! empty($input['allow_stale_primary'])
26112630
);
26122631
}
26132632

@@ -2623,7 +2642,8 @@ public static function listDirectory( array $input ): array|\WP_Error {
26232642

26242643
return $reader->list_directory(
26252644
$input['repo'] ?? '',
2626-
$input['path'] ?? null
2645+
$input['path'] ?? null,
2646+
! empty($input['allow_stale_primary'])
26272647
);
26282648
}
26292649

@@ -2644,7 +2664,8 @@ public static function grepFiles( array $input ): array|\WP_Error {
26442664
$input['path'] ?? null,
26452665
$input['include'] ?? null,
26462666
isset($input['max_results']) ? (int) $input['max_results'] : 100,
2647-
isset($input['context_lines']) ? (int) $input['context_lines'] : 0
2667+
isset($input['context_lines']) ? (int) $input['context_lines'] : 0,
2668+
! empty($input['allow_stale_primary'])
26482669
);
26492670
}
26502671

@@ -2668,7 +2689,8 @@ public static function grepFiles( array $input ): array|\WP_Error {
26682689
$input['path'] ?? null,
26692690
$input['include'] ?? null,
26702691
isset($input['max_results']) ? (int) $input['max_results'] : 100,
2671-
isset($input['context_lines']) ? (int) $input['context_lines'] : 0
2692+
isset($input['context_lines']) ? (int) $input['context_lines'] : 0,
2693+
! empty($input['allow_stale_primary'])
26722694
);
26732695
}
26742696

@@ -3060,7 +3082,7 @@ public static function gitPull( array $input ): array|\WP_Error {
30603082
return $workspace->git_pull(
30613083
$input['name'] ?? '',
30623084
! empty($input['allow_dirty']),
3063-
! empty($input['allow_primary_mutation']),
3085+
! empty($input['allow_primary_refresh']) || ! empty($input['allow_primary_mutation']),
30643086
(string) ( $input['remote'] ?? 'origin' ),
30653087
isset($input['branch']) ? (string) $input['branch'] : null
30663088
);
@@ -3127,7 +3149,7 @@ public static function gitCommit( array $input ): array|\WP_Error {
31273149
return $workspace->git_commit(
31283150
$input['name'] ?? '',
31293151
$input['message'] ?? '',
3130-
! empty($input['allow_primary_mutation'])
3152+
! empty($input['allow_dangerous_primary_mutation'])
31313153
);
31323154
}
31333155

@@ -3152,7 +3174,7 @@ public static function gitPush( array $input ): array|\WP_Error {
31523174
$input['name'] ?? '',
31533175
$input['remote'] ?? 'origin',
31543176
$input['branch'] ?? null,
3155-
! empty($input['allow_primary_mutation']),
3177+
! empty($input['allow_dangerous_primary_mutation']),
31563178
! empty($input['force_with_lease']),
31573179
$input['expected_sha'] ?? null
31583180
);
@@ -3420,7 +3442,7 @@ public static function gitRebase( array $input ): array|\WP_Error {
34203442
$input['onto'] ?? null,
34213443
$input['strategy_option'] ?? null,
34223444
! empty($input['continue']),
3423-
! empty($input['allow_primary_mutation'])
3445+
! empty($input['allow_dangerous_primary_mutation'])
34243446
);
34253447
}
34263448

@@ -3437,7 +3459,7 @@ public static function gitReset( array $input ): array|\WP_Error {
34373459
$input['mode'] ?? 'mixed',
34383460
$input['target'] ?? null,
34393461
! empty($input['allow_destructive']),
3440-
! empty($input['allow_primary_mutation'])
3462+
! empty($input['allow_dangerous_primary_mutation'])
34413463
);
34423464
}
34433465

@@ -3474,7 +3496,7 @@ public static function prRebase( array $input ): array|\WP_Error {
34743496
$input['pr'] ?? null,
34753497
! empty($input['squash']),
34763498
$drop_paths,
3477-
! empty($input['allow_primary_mutation'])
3499+
! empty($input['allow_dangerous_primary_mutation'])
34783500
);
34793501
}
34803502

0 commit comments

Comments
 (0)