gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -1,26 +1,27 @@
1
1
  /**
2
2
  * worktree-sync-milestones.test.ts — Regression test for #1311.
3
3
  *
4
- * Verifies that syncGsdStateToWorktree copies missing milestones,
5
- * milestone files, and slice directories from the main repo's .gsd/
6
- * into the worktree's .gsd/.
4
+ * Verifies that syncProjectRootToWorktree copies milestone artifacts
5
+ * from the main repo's .gsd/ into the worktree's .gsd/ for the
6
+ * specified milestone, and deletes gsd.db so it rebuilds from fresh state.
7
7
  *
8
8
  * Covers:
9
- * - Entirely missing milestone directory
10
- * - Milestone exists but missing CONTEXT/ROADMAP files
11
- * - Missing slices within an existing milestone
12
- * - No-op when directories are identical (symlinked)
13
- * - Root-level files (DECISIONS, REQUIREMENTS, etc.)
9
+ * - Milestone directory synced from main to worktree
10
+ * - Missing slices within a milestone are synced
11
+ * - gsd.db deleted in worktree after sync
12
+ * - No-op when paths are equal
13
+ * - No-op when milestoneId is null
14
+ * - Non-existent directories handled gracefully
14
15
  */
15
16
 
16
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, symlinkSync, realpathSync } from 'node:fs';
17
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
17
18
  import { join } from 'node:path';
18
19
  import { tmpdir } from 'node:os';
19
20
 
20
- import { syncGsdStateToWorktree } from '../auto-worktree.ts';
21
+ import { syncProjectRootToWorktree } from '../auto-worktree-sync.ts';
21
22
  import { createTestContext } from './test-helpers.ts';
22
23
 
23
- const { assertEq, assertTrue, report } = createTestContext();
24
+ const { assertTrue, report } = createTestContext();
24
25
 
25
26
  function createBase(name: string): string {
26
27
  const base = mkdtempSync(join(tmpdir(), `gsd-wt-sync-${name}-`));
@@ -34,156 +35,106 @@ function cleanup(base: string): void {
34
35
 
35
36
  async function main(): Promise<void> {
36
37
 
37
- // ─── 1. Missing milestone directory is synced ─────────────────────────
38
- console.log('\n=== 1. missing milestone directory is copied from main ===');
38
+ // ─── 1. Milestone directory synced from main to worktree ──────────────
39
+ console.log('\n=== 1. milestone directory synced from main to worktree ===');
39
40
  {
40
41
  const mainBase = createBase('main');
41
42
  const wtBase = createBase('wt');
42
43
 
43
44
  try {
44
- // Main repo has M001 and M002
45
45
  const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
46
46
  mkdirSync(m001Dir, { recursive: true });
47
- writeFileSync(join(m001Dir, 'M001-CONTEXT.md'), '# M001\nDone.');
47
+ writeFileSync(join(m001Dir, 'M001-CONTEXT.md'), '# M001\nContext.');
48
48
  writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
49
49
 
50
- const m002Dir = join(mainBase, '.gsd', 'milestones', 'M002');
51
- mkdirSync(m002Dir, { recursive: true });
52
- writeFileSync(join(m002Dir, 'M002-CONTEXT.md'), '# M002\nNew milestone.');
53
- writeFileSync(join(m002Dir, 'M002-ROADMAP.md'), '# Roadmap');
50
+ // Worktree has no M001
51
+ assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'M001')), 'M001 missing before sync');
54
52
 
55
- // Worktree only has M001
56
- const wtM001Dir = join(wtBase, '.gsd', 'milestones', 'M001');
57
- mkdirSync(wtM001Dir, { recursive: true });
58
- writeFileSync(join(wtM001Dir, 'M001-CONTEXT.md'), '# M001\nDone.');
59
-
60
- // M002 is missing from worktree
61
- assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'M002')), 'M002 missing before sync');
53
+ syncProjectRootToWorktree(mainBase, wtBase, 'M001');
62
54
 
63
- const result = syncGsdStateToWorktree(mainBase, wtBase);
64
-
65
- assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M002')), '#1311: M002 synced to worktree');
66
- assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M002', 'M002-CONTEXT.md')), 'M002 CONTEXT synced');
67
- assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M002', 'M002-ROADMAP.md')), 'M002 ROADMAP synced');
68
- assertTrue(result.synced.length > 0, 'sync reported files');
55
+ assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001')), '#1311: M001 synced to worktree');
56
+ assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-CONTEXT.md')), 'M001 CONTEXT synced');
57
+ assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md')), 'M001 ROADMAP synced');
69
58
  } finally {
70
59
  cleanup(mainBase);
71
60
  cleanup(wtBase);
72
61
  }
73
62
  }
74
63
 
75
- // ─── 2. Missing files within existing milestone ───────────────────────
76
- console.log('\n=== 2. missing files within existing milestone are synced ===');
64
+ // ─── 2. Missing slices synced ──────────────────────────────────────────
65
+ console.log('\n=== 2. missing slices within milestone are synced ===');
77
66
  {
78
67
  const mainBase = createBase('main');
79
68
  const wtBase = createBase('wt');
80
69
 
81
70
  try {
82
- // Main repo M001 has CONTEXT, ROADMAP, RESEARCH
83
71
  const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
84
- mkdirSync(m001Dir, { recursive: true });
85
- writeFileSync(join(m001Dir, 'M001-CONTEXT.md'), '# M001 Context');
86
- writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# M001 Roadmap');
87
- writeFileSync(join(m001Dir, 'M001-RESEARCH.md'), '# M001 Research');
72
+ mkdirSync(join(m001Dir, 'slices', 'S01'), { recursive: true });
73
+ mkdirSync(join(m001Dir, 'slices', 'S02'), { recursive: true });
74
+ writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
75
+ writeFileSync(join(m001Dir, 'slices', 'S01', 'S01-PLAN.md'), '# S01 Plan');
76
+ writeFileSync(join(m001Dir, 'slices', 'S02', 'S02-PLAN.md'), '# S02 Plan');
88
77
 
89
- // Worktree M001 only has CONTEXT (stale snapshot)
78
+ // Worktree only has S01
90
79
  const wtM001Dir = join(wtBase, '.gsd', 'milestones', 'M001');
91
- mkdirSync(wtM001Dir, { recursive: true });
92
- writeFileSync(join(wtM001Dir, 'M001-CONTEXT.md'), '# M001 Context');
93
-
94
- const result = syncGsdStateToWorktree(mainBase, wtBase);
95
-
96
- assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md')), 'ROADMAP synced');
97
- assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-RESEARCH.md')), 'RESEARCH synced');
98
- // Existing file should NOT be overwritten
99
- assertEq(
100
- readFileSync(join(wtBase, '.gsd', 'milestones', 'M001', 'M001-CONTEXT.md'), 'utf-8'),
101
- '# M001 Context',
102
- 'existing CONTEXT not overwritten',
103
- );
80
+ mkdirSync(join(wtM001Dir, 'slices', 'S01'), { recursive: true });
81
+ writeFileSync(join(wtM001Dir, 'slices', 'S01', 'S01-PLAN.md'), '# S01 Plan');
82
+
83
+ syncProjectRootToWorktree(mainBase, wtBase, 'M001');
84
+
85
+ assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S02')), '#1311: S02 synced');
86
+ assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'S02-PLAN.md')), 'S02 PLAN synced');
104
87
  } finally {
105
88
  cleanup(mainBase);
106
89
  cleanup(wtBase);
107
90
  }
108
91
  }
109
92
 
110
- // ─── 3. Missing slices directory synced ───────────────────────────────
111
- console.log('\n=== 3. missing slices directory synced ===');
93
+ // ─── 3. gsd.db deleted in worktree after sync ─────────────────────────
94
+ console.log('\n=== 3. gsd.db deleted in worktree after sync ===');
112
95
  {
113
96
  const mainBase = createBase('main');
114
97
  const wtBase = createBase('wt');
115
98
 
116
99
  try {
117
- // Main repo has M001 with slices S01–S03
118
100
  const m001Dir = join(mainBase, '.gsd', 'milestones', 'M001');
119
- mkdirSync(join(m001Dir, 'slices', 'S01'), { recursive: true });
120
- mkdirSync(join(m001Dir, 'slices', 'S02'), { recursive: true });
121
- mkdirSync(join(m001Dir, 'slices', 'S03'), { recursive: true });
101
+ mkdirSync(m001Dir, { recursive: true });
122
102
  writeFileSync(join(m001Dir, 'M001-ROADMAP.md'), '# Roadmap');
123
- writeFileSync(join(m001Dir, 'slices', 'S01', 'S01-PLAN.md'), '# S01 Plan');
124
- writeFileSync(join(m001Dir, 'slices', 'S02', 'S02-PLAN.md'), '# S02 Plan');
125
- writeFileSync(join(m001Dir, 'slices', 'S03', 'S03-PLAN.md'), '# S03 Plan');
126
103
 
127
- // Worktree M001 has slices S01–S02 only (S03 missing)
128
- const wtM001Dir = join(wtBase, '.gsd', 'milestones', 'M001');
129
- mkdirSync(join(wtM001Dir, 'slices', 'S01'), { recursive: true });
130
- mkdirSync(join(wtM001Dir, 'slices', 'S02'), { recursive: true });
131
- writeFileSync(join(wtM001Dir, 'M001-ROADMAP.md'), '# Roadmap');
132
- writeFileSync(join(wtM001Dir, 'slices', 'S01', 'S01-PLAN.md'), '# S01 Plan');
133
- writeFileSync(join(wtM001Dir, 'slices', 'S02', 'S02-PLAN.md'), '# S02 Plan');
104
+ // Worktree has a stale gsd.db
105
+ writeFileSync(join(wtBase, '.gsd', 'gsd.db'), 'stale data');
106
+ assertTrue(existsSync(join(wtBase, '.gsd', 'gsd.db')), 'gsd.db exists before sync');
134
107
 
135
- assertTrue(!existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S03')), 'S03 missing before sync');
108
+ syncProjectRootToWorktree(mainBase, wtBase, 'M001');
136
109
 
137
- syncGsdStateToWorktree(mainBase, wtBase);
138
-
139
- assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S03')), '#1311: S03 synced');
140
- assertTrue(existsSync(join(wtBase, '.gsd', 'milestones', 'M001', 'slices', 'S03', 'S03-PLAN.md')), 'S03 PLAN synced');
110
+ assertTrue(!existsSync(join(wtBase, '.gsd', 'gsd.db')), '#853: gsd.db deleted after sync');
141
111
  } finally {
142
112
  cleanup(mainBase);
143
113
  cleanup(wtBase);
144
114
  }
145
115
  }
146
116
 
147
- // ─── 4. No-op when both resolve to same directory (symlink) ───────────
148
- console.log('\n=== 4. no-op when .gsd/ resolves to same path (symlinked) ===');
117
+ // ─── 4. No-op when paths are equal ────────────────────────────────────
118
+ console.log('\n=== 4. no-op when paths are equal ===');
149
119
  {
150
- const sharedDir = createBase('shared');
151
- const mainBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-main-'));
152
- const wtBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-wt-'));
153
-
120
+ const base = createBase('same');
154
121
  try {
155
- // Both main and worktree symlink to the same shared directory
156
- writeFileSync(join(sharedDir, '.gsd', 'milestones', 'keep'), '');
157
- symlinkSync(join(sharedDir, '.gsd'), join(mainBase, '.gsd'));
158
- symlinkSync(join(sharedDir, '.gsd'), join(wtBase, '.gsd'));
159
-
160
- const result = syncGsdStateToWorktree(mainBase, wtBase);
161
- assertEq(result.synced.length, 0, 'no files synced when both point to same dir');
122
+ // Should not throw
123
+ syncProjectRootToWorktree(base, base, 'M001');
124
+ assertTrue(true, 'no crash when paths are equal');
162
125
  } finally {
163
- cleanup(sharedDir);
164
- rmSync(mainBase, { recursive: true, force: true });
165
- rmSync(wtBase, { recursive: true, force: true });
126
+ cleanup(base);
166
127
  }
167
128
  }
168
129
 
169
- // ─── 5. Root-level .gsd/ files synced ─────────────────────────────────
170
- console.log('\n=== 5. root-level .gsd/ files synced ===');
130
+ // ─── 5. No-op when milestoneId is null ────────────────────────────────
131
+ console.log('\n=== 5. no-op when milestoneId is null ===');
171
132
  {
172
133
  const mainBase = createBase('main');
173
134
  const wtBase = createBase('wt');
174
-
175
135
  try {
176
- writeFileSync(join(mainBase, '.gsd', 'DECISIONS.md'), '# Decisions');
177
- writeFileSync(join(mainBase, '.gsd', 'REQUIREMENTS.md'), '# Requirements');
178
- writeFileSync(join(mainBase, '.gsd', 'PROJECT.md'), '# Project');
179
-
180
- // Worktree has none of these
181
- const result = syncGsdStateToWorktree(mainBase, wtBase);
182
-
183
- assertTrue(existsSync(join(wtBase, '.gsd', 'DECISIONS.md')), 'DECISIONS.md synced');
184
- assertTrue(existsSync(join(wtBase, '.gsd', 'REQUIREMENTS.md')), 'REQUIREMENTS.md synced');
185
- assertTrue(existsSync(join(wtBase, '.gsd', 'PROJECT.md')), 'PROJECT.md synced');
186
- assertTrue(result.synced.length >= 3, 'at least 3 files synced');
136
+ syncProjectRootToWorktree(mainBase, wtBase, null);
137
+ assertTrue(true, 'no crash when milestoneId is null');
187
138
  } finally {
188
139
  cleanup(mainBase);
189
140
  cleanup(wtBase);
@@ -193,8 +144,8 @@ async function main(): Promise<void> {
193
144
  // ─── 6. Non-existent directories handled gracefully ───────────────────
194
145
  console.log('\n=== 6. non-existent directories → no-op ===');
195
146
  {
196
- const result = syncGsdStateToWorktree('/tmp/does-not-exist-main', '/tmp/does-not-exist-wt');
197
- assertEq(result.synced.length, 0, 'no crash on missing directories');
147
+ syncProjectRootToWorktree('/tmp/does-not-exist-main', '/tmp/does-not-exist-wt', 'M001');
148
+ assertTrue(true, 'no crash on missing directories');
198
149
  }
199
150
 
200
151
  report();
@@ -104,11 +104,15 @@ async function main(): Promise<void> {
104
104
  run("git checkout -b f-123-thing", repo);
105
105
  assertEq(getCurrentBranch(repo), "f-123-thing", "on feature branch");
106
106
 
107
+ const commitsBefore = run("git rev-list --count HEAD", repo);
107
108
  captureIntegrationBranch(repo, "M001");
108
109
  assertEq(readIntegrationBranch(repo, "M001"), "f-123-thing",
109
110
  "captureIntegrationBranch records the current branch");
110
111
 
111
- // .gsd/ metadata is written to disk only (not committed) since commit_docs removal
112
+ // Metadata is stored in external state, not committed to git.
113
+ const commitsAfter = run("git rev-list --count HEAD", repo);
114
+ assertEq(commitsAfter, commitsBefore, "captureIntegrationBranch does not create a git commit");
115
+
112
116
  rmSync(repo, { recursive: true, force: true });
113
117
  }
114
118
 
@@ -1,31 +1,19 @@
1
1
  /**
2
- * Unit tests for the CONTEXT.md write-gate.
2
+ * Unit tests for the CONTEXT.md write-gate (D031 guard chain).
3
3
  *
4
4
  * Exercises shouldBlockContextWrite() — a pure function that implements:
5
5
  * (a) toolName !== "write" → pass
6
- * (b) milestoneId null AND no queue phase → pass (not in any flow)
6
+ * (b) milestoneId null → pass (not in discussion)
7
7
  * (c) path doesn't match /M\d+-CONTEXT\.md$/ → pass
8
- * (d) depthVerified → pass (backward compat for discussion flows)
9
- * (e) queuePhaseActive + per-milestone verified pass
10
- * (f) queuePhaseActive + not verified → block
11
- * (g) else → block with actionable reason
12
- *
13
- * Also exercises per-milestone verification helpers:
14
- * markDepthVerified(), isDepthVerifiedFor()
8
+ * (d) depthVerified → pass
9
+ * (e) else block with actionable reason
15
10
  */
16
11
 
17
12
  import test from 'node:test';
18
13
  import assert from 'node:assert/strict';
19
- import {
20
- shouldBlockContextWrite,
21
- markDepthVerified,
22
- isDepthVerifiedFor,
23
- isDepthVerified,
24
- } from '../index.ts';
14
+ import { shouldBlockContextWrite } from '../index.ts';
25
15
 
26
- // ═══════════════════════════════════════════════════════════════════════════
27
- // Discussion flow tests (backward compatibility)
28
- // ═══════════════════════════════════════════════════════════════════════════
16
+ // ─── Scenario 1: Blocks CONTEXT.md write during discussion without depth verification (absolute path) ──
29
17
 
30
18
  test('write-gate: blocks CONTEXT.md write during discussion without depth verification (absolute path)', () => {
31
19
  const result = shouldBlockContextWrite(
@@ -38,6 +26,8 @@ test('write-gate: blocks CONTEXT.md write during discussion without depth verifi
38
26
  assert.ok(result.reason, 'should provide a reason');
39
27
  });
40
28
 
29
+ // ─── Scenario 2: Blocks CONTEXT.md write during discussion without depth verification (relative path) ──
30
+
41
31
  test('write-gate: blocks CONTEXT.md write during discussion without depth verification (relative path)', () => {
42
32
  const result = shouldBlockContextWrite(
43
33
  'write',
@@ -49,7 +39,9 @@ test('write-gate: blocks CONTEXT.md write during discussion without depth verifi
49
39
  assert.ok(result.reason, 'should provide a reason');
50
40
  });
51
41
 
52
- test('write-gate: allows CONTEXT.md write after depth verification (discussion flow)', () => {
42
+ // ─── Scenario 3: Allows CONTEXT.md write after depth verification ──
43
+
44
+ test('write-gate: allows CONTEXT.md write after depth verification', () => {
53
45
  const result = shouldBlockContextWrite(
54
46
  'write',
55
47
  '/Users/dev/project/.gsd/milestones/M001/M001-CONTEXT.md',
@@ -60,152 +52,71 @@ test('write-gate: allows CONTEXT.md write after depth verification (discussion f
60
52
  assert.strictEqual(result.reason, undefined, 'should have no reason');
61
53
  });
62
54
 
63
- test('write-gate: allows CONTEXT.md write outside any flow (milestoneId null, no queue)', () => {
55
+ // ─── Scenario 4: Allows CONTEXT.md write outside discussion phase (milestoneId null) ──
56
+
57
+ test('write-gate: allows CONTEXT.md write outside discussion phase', () => {
64
58
  const result = shouldBlockContextWrite(
65
59
  'write',
66
60
  '.gsd/milestones/M001/M001-CONTEXT.md',
67
61
  null,
68
62
  false,
69
- false,
70
63
  );
71
- assert.strictEqual(result.block, false, 'should not block outside any flow');
64
+ assert.strictEqual(result.block, false, 'should not block outside discussion phase');
72
65
  });
73
66
 
74
- test('write-gate: allows non-CONTEXT.md writes during discussion', () => {
75
- const r1 = shouldBlockContextWrite('write', '.gsd/milestones/M001/M001-DISCUSSION.md', 'M001', false);
76
- assert.strictEqual(r1.block, false, 'DISCUSSION.md should pass');
77
-
78
- const r2 = shouldBlockContextWrite('write', '.gsd/milestones/M001/slices/S01/S01-PLAN.md', 'M001', false);
79
- assert.strictEqual(r2.block, false, 'slice plan should pass');
80
-
81
- const r3 = shouldBlockContextWrite('write', 'src/index.ts', 'M001', false);
82
- assert.strictEqual(r3.block, false, 'regular code file should pass');
83
- });
67
+ // ─── Scenario 5: Allows non-CONTEXT.md writes during discussion ──
84
68
 
85
- test('write-gate: regex does not match slice context files (S01-CONTEXT.md)', () => {
86
- const result = shouldBlockContextWrite(
69
+ test('write-gate: allows non-CONTEXT.md writes during discussion', () => {
70
+ // DISCUSSION.md
71
+ const r1 = shouldBlockContextWrite(
87
72
  'write',
88
- '.gsd/milestones/M001/slices/S01/S01-CONTEXT.md',
73
+ '.gsd/milestones/M001/M001-DISCUSSION.md',
89
74
  'M001',
90
75
  false,
91
76
  );
92
- assert.strictEqual(result.block, false, 'S01-CONTEXT.md should not be blocked');
93
- });
94
-
95
- test('write-gate: blocked reason contains actionable instructions', () => {
96
- const result = shouldBlockContextWrite(
97
- 'write',
98
- '.gsd/milestones/M999/M999-CONTEXT.md',
99
- 'M999',
100
- false,
101
- );
102
- assert.strictEqual(result.block, true);
103
- assert.ok(result.reason!.includes('depth_verification'), 'reason should mention depth_verification');
104
- assert.ok(result.reason!.includes('ask_user_questions'), 'reason should mention ask_user_questions');
105
- });
106
-
107
- // ═══════════════════════════════════════════════════════════════════════════
108
- // Queue flow tests (NEW — enforces write-gate during /gsd queue)
109
- // ═══════════════════════════════════════════════════════════════════════════
110
-
111
- test('write-gate: blocks CONTEXT.md write during queue flow without verification', () => {
112
- const result = shouldBlockContextWrite(
113
- 'write',
114
- '.gsd/milestones/M010-3ym37m/M010-3ym37m-CONTEXT.md',
115
- null, // queue flows have no pendingAutoStart → milestoneId is null
116
- false,
117
- true, // but queuePhaseActive is true
118
- );
119
- assert.strictEqual(result.block, true, 'should block during queue flow without verification');
120
- assert.ok(result.reason!.includes('multi-milestone'), 'reason should mention multi-milestone');
121
- });
122
-
123
- test('write-gate: allows CONTEXT.md write during queue flow AFTER per-milestone verification', () => {
124
- // Simulate: depth_verification_M010-3ym37m was answered
125
- markDepthVerified('M010-3ym37m');
77
+ assert.strictEqual(r1.block, false, 'DISCUSSION.md should pass');
126
78
 
127
- const result = shouldBlockContextWrite(
79
+ // Slice file
80
+ const r2 = shouldBlockContextWrite(
128
81
  'write',
129
- '.gsd/milestones/M010-3ym37m/M010-3ym37m-CONTEXT.md',
130
- null,
82
+ '.gsd/milestones/M001/slices/S01/S01-PLAN.md',
83
+ 'M001',
131
84
  false,
132
- true,
133
85
  );
134
- assert.strictEqual(result.block, false, 'should allow after per-milestone verification');
135
- });
86
+ assert.strictEqual(r2.block, false, 'slice plan should pass');
136
87
 
137
- test('write-gate: blocks DIFFERENT milestone in queue flow when only one is verified', () => {
138
- // M010-3ym37m was verified above, but M011-rfmd3q was NOT
139
- const result = shouldBlockContextWrite(
88
+ // Regular code file
89
+ const r3 = shouldBlockContextWrite(
140
90
  'write',
141
- '.gsd/milestones/M011-rfmd3q/M011-rfmd3q-CONTEXT.md',
142
- null,
91
+ 'src/index.ts',
92
+ 'M001',
143
93
  false,
144
- true,
145
94
  );
146
- assert.strictEqual(result.block, true, 'should block unverified milestone even when another is verified');
95
+ assert.strictEqual(r3.block, false, 'regular code file should pass');
147
96
  });
148
97
 
149
- test('write-gate: wildcard verification unlocks all milestones in queue flow', () => {
150
- markDepthVerified('*');
151
-
152
- const r1 = shouldBlockContextWrite(
153
- 'write',
154
- '.gsd/milestones/M099/M099-CONTEXT.md',
155
- null,
156
- false,
157
- true,
158
- );
159
- assert.strictEqual(r1.block, false, 'wildcard should pass any milestone');
160
- });
98
+ // ─── Scenario 6: Regex specificity doesn't match S01-CONTEXT.md ──
161
99
 
162
- test('write-gate: allows non-CONTEXT.md writes during queue flow regardless', () => {
100
+ test('write-gate: regex does not match slice context files (S01-CONTEXT.md)', () => {
163
101
  const result = shouldBlockContextWrite(
164
102
  'write',
165
- '.gsd/QUEUE.md',
166
- null,
103
+ '.gsd/milestones/M001/slices/S01/S01-CONTEXT.md',
104
+ 'M001',
167
105
  false,
168
- true,
169
106
  );
170
- assert.strictEqual(result.block, false, 'QUEUE.md should pass during queue flow');
107
+ assert.strictEqual(result.block, false, 'S01-CONTEXT.md should not be blocked');
171
108
  });
172
109
 
173
- // ═══════════════════════════════════════════════════════════════════════════
174
- // Unique milestone ID format tests
175
- // ═══════════════════════════════════════════════════════════════════════════
176
-
177
- test('write-gate: matches unique milestone ID format (M010-3ym37m)', () => {
178
- const result = shouldBlockContextWrite(
179
- 'write',
180
- '.gsd/milestones/M010-3ym37m/M010-3ym37m-CONTEXT.md',
181
- 'M010-3ym37m',
182
- false,
183
- );
184
- assert.strictEqual(result.block, true, 'should match unique milestone ID format');
185
- });
110
+ // ─── Scenario 7: Error message contains actionable instruction ──
186
111
 
187
- test('write-gate: matches classic milestone ID format (M001)', () => {
112
+ test('write-gate: blocked reason contains depth_verification keyword', () => {
188
113
  const result = shouldBlockContextWrite(
189
114
  'write',
190
- '.gsd/milestones/M001/M001-CONTEXT.md',
191
- 'M001',
115
+ '.gsd/milestones/M999/M999-CONTEXT.md',
116
+ 'M999',
192
117
  false,
193
118
  );
194
- assert.strictEqual(result.block, true, 'should match classic milestone ID format');
195
- });
196
-
197
- // ═══════════════════════════════════════════════════════════════════════════
198
- // Per-milestone depth verification helpers
199
- // ═══════════════════════════════════════════════════════════════════════════
200
-
201
- test('isDepthVerifiedFor: returns false for unknown milestone', () => {
202
- assert.strictEqual(isDepthVerifiedFor('M999-xxxxxx'), true,
203
- 'returns true because wildcard * was set in earlier test');
204
- // Note: test isolation would require clearing state, but these tests
205
- // exercise the module as a singleton (matching production behavior)
206
- });
207
-
208
- test('isDepthVerified: returns true when any milestone verified', () => {
209
- // At this point M010-3ym37m and * are verified from earlier tests
210
- assert.strictEqual(isDepthVerified(), true);
119
+ assert.strictEqual(result.block, true);
120
+ assert.ok(result.reason!.includes('depth_verification'), 'reason should mention depth_verification question id');
121
+ assert.ok(result.reason!.includes('ask_user_questions'), 'reason should mention ask_user_questions tool');
211
122
  });