gsd-pi 2.82.0-dev.2841a1e44 → 2.82.0-dev.9d5798940

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 (118) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/partial-builder.js +2 -1
  3. package/dist/resources/extensions/gsd/auto-dispatch.js +13 -6
  4. package/dist/resources/extensions/gsd/auto-post-unit.js +69 -8
  5. package/dist/resources/extensions/gsd/auto-recovery.js +31 -1
  6. package/dist/resources/extensions/gsd/auto-start.js +7 -3
  7. package/dist/resources/extensions/gsd/auto-worktree.js +96 -0
  8. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +4 -1
  9. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +13 -1
  10. package/dist/resources/extensions/gsd/commands/handlers/core.js +17 -1
  11. package/dist/resources/extensions/gsd/db/unit-dispatches.js +2 -2
  12. package/dist/resources/extensions/gsd/export-html.js +27 -425
  13. package/dist/resources/extensions/gsd/milestone-actions.js +11 -4
  14. package/dist/resources/extensions/gsd/native-git-bridge.js +8 -3
  15. package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +6 -1
  16. package/dist/resources/extensions/gsd/tools/plan-slice.js +2 -1
  17. package/dist/resources/extensions/gsd/unit-context-manifest.js +7 -8
  18. package/dist/resources/extensions/gsd/worktree-lifecycle.js +28 -7
  19. package/dist/resources/extensions/shared/html-shell.js +388 -0
  20. package/dist/resources/extensions/visual-brief/page-contract.js +2 -0
  21. package/dist/resources/extensions/visual-brief/prompts.js +29 -0
  22. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  23. package/dist/web/standalone/.next/BUILD_ID +1 -1
  24. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  25. package/dist/web/standalone/.next/build-manifest.json +3 -3
  26. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  37. package/dist/web/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.rsc +4 -7
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +4 -5
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -5
  47. package/dist/web/standalone/.next/server/app/index.html +1 -1
  48. package/dist/web/standalone/.next/server/app/index.rsc +4 -7
  49. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -7
  51. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +4 -5
  53. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -5
  54. package/dist/web/standalone/.next/server/app/page.js +2 -2
  55. package/dist/web/standalone/.next/server/app/page.js.nft.json +1 -1
  56. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  57. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  58. package/dist/web/standalone/.next/server/chunks/4266.js +2 -0
  59. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  61. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  62. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  63. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  64. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  65. package/dist/web/standalone/.next/static/chunks/app/layout-8c10ec293ae0f1d5.js +1 -0
  66. package/dist/web/standalone/.next/static/chunks/{webpack-6a95bc41e0f7ec89.js → webpack-9a4db269f9ed63ad.js} +1 -1
  67. package/dist/web/standalone/.next/static/css/746ee28c929d1880.css +1 -0
  68. package/package.json +1 -1
  69. package/src/resources/extensions/claude-code-cli/partial-builder.ts +2 -1
  70. package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +19 -2
  71. package/src/resources/extensions/gsd/auto-dispatch.ts +14 -6
  72. package/src/resources/extensions/gsd/auto-post-unit.ts +76 -6
  73. package/src/resources/extensions/gsd/auto-recovery.ts +29 -0
  74. package/src/resources/extensions/gsd/auto-start.ts +7 -3
  75. package/src/resources/extensions/gsd/auto-worktree.ts +104 -0
  76. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +6 -1
  77. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +16 -1
  78. package/src/resources/extensions/gsd/commands/handlers/core.ts +17 -1
  79. package/src/resources/extensions/gsd/db/unit-dispatches.ts +3 -3
  80. package/src/resources/extensions/gsd/export-html.ts +27 -427
  81. package/src/resources/extensions/gsd/milestone-actions.ts +10 -4
  82. package/src/resources/extensions/gsd/native-git-bridge.ts +8 -3
  83. package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +8 -1
  84. package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts +116 -0
  85. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +12 -1
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +15 -1
  87. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +69 -1
  88. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +57 -2
  89. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +39 -0
  90. package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +8 -0
  91. package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +55 -1
  92. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +25 -0
  93. package/src/resources/extensions/gsd/tests/post-unit-git-failure.test.ts +1 -1
  94. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +46 -2
  95. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +10 -0
  96. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +39 -0
  97. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +65 -7
  98. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +64 -12
  99. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +31 -0
  100. package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
  101. package/src/resources/extensions/gsd/unit-context-manifest.ts +12 -9
  102. package/src/resources/extensions/gsd/worktree-lifecycle.ts +34 -7
  103. package/src/resources/extensions/shared/html-shell.ts +412 -0
  104. package/src/resources/extensions/visual-brief/page-contract.ts +2 -0
  105. package/src/resources/extensions/visual-brief/prompts.ts +37 -1
  106. package/src/resources/extensions/visual-brief/tests/visual-brief.test.ts +40 -0
  107. package/dist/web/standalone/.next/server/chunks/5822.js +0 -2
  108. package/dist/web/standalone/.next/static/chunks/app/layout-a16c7a7ecdf0c2cf.js +0 -1
  109. package/dist/web/standalone/.next/static/css/0262768ec1b89d34.css +0 -1
  110. package/dist/web/standalone/.next/static/css/de70bee13400563f.css +0 -1
  111. package/dist/web/standalone/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  112. package/dist/web/standalone/.next/static/media/747892c23ea88013-s.woff2 +0 -0
  113. package/dist/web/standalone/.next/static/media/8d697b304b401681-s.woff2 +0 -0
  114. package/dist/web/standalone/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
  115. package/dist/web/standalone/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
  116. package/dist/web/standalone/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
  117. /package/dist/web/standalone/.next/static/{Qgr2B_MRhPxC0z8fwv4vT → BdZQhe8yKl6bdKLiXVEzh}/_buildManifest.js +0 -0
  118. /package/dist/web/standalone/.next/static/{Qgr2B_MRhPxC0z8fwv4vT → BdZQhe8yKl6bdKLiXVEzh}/_ssgManifest.js +0 -0
@@ -14,7 +14,10 @@ import {
14
14
  type SkillsPolicy,
15
15
  type UnitContextManifest,
16
16
  } from "../unit-context-manifest.ts";
17
- import { ALLOWED_PLANNING_DISPATCH_AGENTS } from "../bootstrap/write-gate.ts";
17
+ import {
18
+ ALLOWED_PLANNING_DISPATCH_AGENTS,
19
+ shouldBlockPlanningUnit,
20
+ } from "../bootstrap/write-gate.ts";
18
21
  import {
19
22
  getRequiredWorkflowToolsForAutoUnit,
20
23
  getRequiredWorkflowToolsForGuidedUnit,
@@ -217,7 +220,7 @@ test("#4934: every manifest declares a tools policy", () => {
217
220
  });
218
221
 
219
222
  test("#4934: tools.mode is one of the declared policies", () => {
220
- const validModes = new Set(["all", "read-only", "planning", "planning-dispatch", "docs"]);
223
+ const validModes = new Set(["all", "read-only", "planning", "planning-dispatch", "docs", "verification"]);
221
224
  for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
222
225
  const mode = (manifest as { tools: { mode: string } }).tools.mode;
223
226
  assert.ok(
@@ -227,28 +230,83 @@ test("#4934: tools.mode is one of the declared policies", () => {
227
230
  }
228
231
  });
229
232
 
230
- test('#4934: only execute-task and reactive-execute may use tools.mode "all" (full source-tree write access)', () => {
231
- const allowedAllUnits = new Set(["execute-task", "reactive-execute"]);
233
+ test('#4934: only execution units and complete-milestone may use tools.mode "all"', () => {
234
+ const allowedAllUnits = new Set(["execute-task", "reactive-execute", "complete-milestone"]);
232
235
  for (const [unitType, manifest] of Object.entries(UNIT_MANIFESTS)) {
233
236
  const mode = (manifest as { tools: { mode: string } }).tools.mode;
234
237
  if (mode === "all") {
235
238
  assert.ok(
236
239
  allowedAllUnits.has(unitType),
237
- `manifest "${unitType}" declares tools.mode = "all" but is not on the execute-track. ` +
238
- 'Only execute-task and reactive-execute should have full source write access; ' +
240
+ `manifest "${unitType}" declares tools.mode = "all" but is not explicitly allowed. ` +
241
+ 'Only execute-task, reactive-execute, and complete-milestone should have full source write access; ' +
239
242
  'planning/discuss/research units must use "planning" or "planning-dispatch" (or "docs" for rewrite-docs).',
240
243
  );
241
244
  }
242
245
  }
243
246
  });
244
247
 
248
+ test("#5453: complete-milestone uses all tools so bash verification is not planning-dispatch blocked", () => {
249
+ const manifest = UNIT_MANIFESTS["complete-milestone"];
250
+
251
+ assert.strictEqual(manifest.tools.mode, "all");
252
+ assert.deepEqual(resolveSubagentPermissionContract("complete-milestone"), {
253
+ allowed: true,
254
+ allowedSubagents: ["*"],
255
+ toolsMode: "all",
256
+ });
257
+ // Runtime gate-level regression: these verification commands were blocked
258
+ // under planning-dispatch in #5453; complete-milestone must bypass that gate.
259
+ for (const cmd of ["git diff --name-only HEAD~1", "git log -n1 --oneline"]) {
260
+ const result = shouldBlockPlanningUnit(
261
+ "bash",
262
+ cmd,
263
+ process.cwd(),
264
+ "complete-milestone",
265
+ manifest.tools,
266
+ );
267
+ assert.strictEqual(
268
+ result.block,
269
+ false,
270
+ `shouldBlockPlanningUnit must not block ${cmd} for complete-milestone: ${result.reason}`,
271
+ );
272
+ }
273
+ });
274
+
275
+ test("#5843: run-uat uses verification tools policy so build/test commands can run", () => {
276
+ const manifest = UNIT_MANIFESTS["run-uat"];
277
+
278
+ assert.strictEqual(manifest.tools.mode, "verification");
279
+
280
+ const buildResult = shouldBlockPlanningUnit(
281
+ "bash",
282
+ "npm run build 2>&1",
283
+ process.cwd(),
284
+ "run-uat",
285
+ manifest.tools,
286
+ );
287
+ assert.strictEqual(
288
+ buildResult.block,
289
+ false,
290
+ `run-uat must allow build verification commands: ${buildResult.reason}`,
291
+ );
292
+
293
+ const sourceWriteResult = shouldBlockPlanningUnit(
294
+ "edit",
295
+ "src/main.ts",
296
+ process.cwd(),
297
+ "run-uat",
298
+ manifest.tools,
299
+ );
300
+ assert.strictEqual(sourceWriteResult.block, true);
301
+ assert.match(sourceWriteResult.reason!, /tools-policy "verification"/);
302
+ });
303
+
245
304
  test('planning-dispatch mode is reserved for slice-level decomposition and completion units', () => {
246
305
  const allowedDispatchUnits = new Set([
247
306
  "plan-slice",
248
307
  "research-slice",
249
308
  "refine-slice",
250
309
  "complete-slice",
251
- "complete-milestone",
252
310
  "gate-evaluate",
253
311
  // Deep planning mode: research-project orchestrates 4 parallel research
254
312
  // subagents (stack/features/architecture/pitfalls). Subagent dispatch is
@@ -30,11 +30,13 @@ function initGitRepoIn(base: string, isolation: "worktree" | "branch" | "none"):
30
30
  }
31
31
  import {
32
32
  WorktreeLifecycle,
33
+ resetRecentWorktreeMergeFailuresForTest,
33
34
  type WorktreeLifecycleDeps,
34
35
  type NotifyCtx,
35
36
  } from "../worktree-lifecycle.js";
36
37
  import { WorktreeStateProjection } from "../worktree-state-projection.js";
37
38
  import { type TaskCommitContext } from "../worktree.js";
39
+ import { MergeConflictError } from "../git-service.js";
38
40
 
39
41
  // ADR-016 phase 2 / C-track retired all worktree-manager + cache + prefs
40
42
  // fields from `WorktreeLifecycleDeps`. Tests still pass them as overrides
@@ -141,6 +143,20 @@ function readJournalEntries(basePath: string): JournalEntry[] {
141
143
  }
142
144
  }
143
145
 
146
+ function setupMergeWorktree(basePath: string, milestoneId: string): string {
147
+ initGitRepoIn(basePath, "worktree");
148
+ execFileSync("git", ["checkout", "-b", `milestone/${milestoneId}`], { cwd: basePath, stdio: "pipe" });
149
+ execFileSync("git", ["checkout", "main"], { cwd: basePath, stdio: "pipe" });
150
+ const wt = join(basePath, ".gsd", "worktrees", milestoneId);
151
+ execFileSync("git", ["worktree", "add", wt, `milestone/${milestoneId}`], { cwd: basePath, stdio: "pipe" });
152
+ mkdirSync(join(basePath, ".gsd", "milestones", milestoneId), { recursive: true });
153
+ writeFileSync(
154
+ join(basePath, ".gsd", "milestones", milestoneId, `${milestoneId}-ROADMAP.md`),
155
+ `# ${milestoneId}\n- [x] S01: Slice one\n`,
156
+ );
157
+ return wt;
158
+ }
159
+
144
160
  // ─── Tests ───────────────────────────────────────────────────────────────────
145
161
 
146
162
  describe("worktree journal events", () => {
@@ -148,6 +164,7 @@ describe("worktree journal events", () => {
148
164
  const originalCwd = process.cwd();
149
165
 
150
166
  beforeEach(() => {
167
+ resetRecentWorktreeMergeFailuresForTest();
151
168
  // realpathSync to match what `auto-worktree.ts` returns from
152
169
  // `resolveWorktreeProjectRoot` (macOS resolves `/var` → `/private/var`).
153
170
  tmp = realpathSync(mkdtempSync(join(tmpdir(), "wt-journal-")));
@@ -284,17 +301,7 @@ describe("worktree journal events", () => {
284
301
  });
285
302
 
286
303
  test("mergeAndExit emits worktree-merge-failed on error", () => {
287
- initGitRepoIn(tmp, "worktree");
288
- execFileSync("git", ["checkout", "-b", "milestone/M001"], { cwd: tmp, stdio: "pipe" });
289
- execFileSync("git", ["checkout", "main"], { cwd: tmp, stdio: "pipe" });
290
- const wt = join(tmp, ".gsd", "worktrees", "M001");
291
- execFileSync("git", ["worktree", "add", wt, "milestone/M001"], { cwd: tmp, stdio: "pipe" });
292
- mkdirSync(join(tmp, ".gsd", "milestones", "M001"), { recursive: true });
293
- writeFileSync(
294
- join(tmp, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
295
- "# M001\n- [x] S01: Slice one\n",
296
- );
297
-
304
+ const wt = setupMergeWorktree(tmp, "M001");
298
305
  const s = makeSession({ basePath: wt, originalBasePath: tmp });
299
306
  const deps = makeDeps({
300
307
  mergeMilestoneToMain: () => { throw new Error("conflict in main"); },
@@ -318,11 +325,56 @@ describe("worktree journal events", () => {
318
325
  );
319
326
  }
320
327
 
328
+ new WorktreeLifecycle(s, deps).exitMilestone(
329
+ "M001",
330
+ { merge: true },
331
+ makeNotifyCtx(),
332
+ );
333
+
321
334
  const entries = readJournalEntries(tmp);
322
- const failed = entries.find(e => e.eventType === "worktree-merge-failed");
335
+ const failures = entries.filter(e => e.eventType === "worktree-merge-failed");
336
+ const failed = failures[0];
323
337
  assert.ok(failed, "worktree-merge-failed event should be emitted");
324
338
  assert.equal(failed!.data?.milestoneId, "M001");
325
339
  assert.equal(failed!.data?.error, "conflict in main");
340
+ assert.equal(failures.length, 1, "duplicate merge failures are journaled once");
341
+ });
342
+
343
+ test("merge failure dedupe uses stable conflict category and expires", (t) => {
344
+ let now = 1_000_000;
345
+ t.mock.method(Date, "now", () => now);
346
+ const wt = setupMergeWorktree(tmp, "M001");
347
+ const s = makeSession({ basePath: wt, originalBasePath: tmp });
348
+ let attempt = 0;
349
+ const deps = makeDeps({
350
+ mergeMilestoneToMain: () => {
351
+ attempt += 1;
352
+ throw new MergeConflictError(
353
+ attempt === 1 ? ["src/a.ts"] : ["src/b.ts", "src/c.ts"],
354
+ "squash",
355
+ "milestone/M001",
356
+ "main",
357
+ );
358
+ },
359
+ });
360
+ const lifecycle = new WorktreeLifecycle(s, deps);
361
+
362
+ lifecycle.exitMilestone("M001", { merge: true }, makeNotifyCtx());
363
+ lifecycle.exitMilestone("M001", { merge: true }, makeNotifyCtx());
364
+
365
+ let failures = readJournalEntries(tmp).filter(e => e.eventType === "worktree-merge-failed");
366
+ assert.equal(failures.length, 1, "variable conflict filenames should not bypass dedupe");
367
+ assert.match(
368
+ String(failures[0]!.data?.error),
369
+ /src\/a\.ts/,
370
+ "journal payload keeps the original error message",
371
+ );
372
+
373
+ now += 60_001;
374
+ lifecycle.exitMilestone("M001", { merge: true }, makeNotifyCtx());
375
+
376
+ failures = readJournalEntries(tmp).filter(e => e.eventType === "worktree-merge-failed");
377
+ assert.equal(failures.length, 2, "same merge failure is journaled again after dedupe expiry");
326
378
  });
327
379
 
328
380
  test("journal entries have valid flowId, seq, and ts fields", () => {
@@ -26,6 +26,7 @@ const PLANNING_DISPATCH_REVIEW: ToolsPolicy = {
26
26
  };
27
27
  const READ_ONLY: ToolsPolicy = { mode: 'read-only' };
28
28
  const ALL: ToolsPolicy = { mode: 'all' };
29
+ const VERIFICATION: ToolsPolicy = { mode: 'verification' };
29
30
  const DOCS: ToolsPolicy = {
30
31
  mode: 'docs',
31
32
  allowedPathGlobs: ['docs/**', 'README.md', 'README.*.md', 'CHANGELOG.md', '*.md'],
@@ -331,6 +332,36 @@ test('all-mode: execute-task can dispatch subagents', () => {
331
332
  assert.strictEqual(r.block, false);
332
333
  });
333
334
 
335
+ // ─── verification mode: bash allowed, writes still scoped ─────────────────
336
+
337
+ test('verification-mode: run-uat can run build commands', () => {
338
+ const r = shouldBlockPlanningUnit('bash', 'npm run build 2>&1', BASE, 'run-uat', VERIFICATION);
339
+ assert.strictEqual(r.block, false);
340
+ });
341
+
342
+ test('verification-mode: run-uat blocks destructive bash (rm -rf)', () => {
343
+ const r = shouldBlockPlanningUnit('bash', 'rm -rf dist', BASE, 'run-uat', VERIFICATION);
344
+ assert.strictEqual(r.block, true);
345
+ assert.match(r.reason!, /bash is restricted to build\/test verification commands/);
346
+ });
347
+
348
+ test('verification-mode: run-uat allows read-only investigative bash (git status)', () => {
349
+ const r = shouldBlockPlanningUnit('bash', 'git status', BASE, 'run-uat', VERIFICATION);
350
+ assert.strictEqual(r.block, false);
351
+ });
352
+
353
+ test('verification-mode: run-uat still blocks user source edits', () => {
354
+ const r = shouldBlockPlanningUnit('edit', join(BASE, 'src', 'main.ts'), BASE, 'run-uat', VERIFICATION);
355
+ assert.strictEqual(r.block, true);
356
+ assert.match(r.reason!, /tools-policy "verification"/);
357
+ });
358
+
359
+ test('verification-mode: run-uat still blocks subagent dispatch', () => {
360
+ const r = shouldBlockPlanningUnit('subagent', '', BASE, 'run-uat', VERIFICATION);
361
+ assert.strictEqual(r.block, true);
362
+ assert.match(r.reason!, /subagent dispatch is not permitted/);
363
+ });
364
+
334
365
  // ─── read-only mode ───────────────────────────────────────────────────────
335
366
 
336
367
  test('read-only: blocks any edit even to .gsd/', () => {
@@ -10,6 +10,7 @@ import {
10
10
  upsertTaskPlanning,
11
11
  insertGateRow,
12
12
  updateSliceStatus,
13
+ setSliceSketchFlag,
13
14
  } from "../gsd-db.js";
14
15
  import type { GateId } from "../types.js";
15
16
  import { invalidateStateCache } from "../state.js";
@@ -184,6 +185,7 @@ export async function handlePlanSlice(
184
185
  if (isDeferredStatus(parentSlice.status)) {
185
186
  updateSliceStatus(params.milestoneId, params.sliceId, "pending");
186
187
  }
188
+ setSliceSketchFlag(params.milestoneId, params.sliceId, false);
187
189
 
188
190
  upsertSlicePlanning(params.milestoneId, params.sliceId, {
189
191
  goal: params.goal,
@@ -132,6 +132,9 @@ export type ContextModePolicy =
132
132
  * the explicit `allowedPathGlobs` set; Bash safe-allowlist;
133
133
  * no subagents. Reserved for rewrite-docs, which legitimately
134
134
  * edits project markdown outside .gsd/.
135
+ * - "verification"
136
+ * — Read tools + Bash for verification commands, writes
137
+ * restricted to .gsd/**, no subagents.
135
138
  *
136
139
  * The allowlist for "docs" is declared per-manifest rather than hardcoded so
137
140
  * projects with non-standard doc layouts can extend it without forking the
@@ -143,7 +146,8 @@ export type ToolsPolicy =
143
146
  | { readonly mode: "read-only" }
144
147
  | { readonly mode: "planning" }
145
148
  | { readonly mode: "planning-dispatch"; readonly allowedSubagents: readonly string[] }
146
- | { readonly mode: "docs"; readonly allowedPathGlobs: readonly string[] };
149
+ | { readonly mode: "docs"; readonly allowedPathGlobs: readonly string[] }
150
+ | { readonly mode: "verification" };
147
151
 
148
152
  // ─── Computed-artifact registry (#4924 v2 contract) ───────────────────────
149
153
 
@@ -288,6 +292,7 @@ const COMMON_BUDGET_SMALL = 250_000; // ~65K tokens
288
292
 
289
293
  const TOOLS_ALL: ToolsPolicy = { mode: "all" };
290
294
  const TOOLS_PLANNING: ToolsPolicy = { mode: "planning" };
295
+ const TOOLS_VERIFICATION: ToolsPolicy = { mode: "verification" };
291
296
  // Like TOOLS_PLANNING but permits dispatch to read-only recon/planning
292
297
  // specialists. Runtime-enforced by write-gate.ts before the subagent tool runs.
293
298
  const TOOLS_PLANNING_DISPATCH_RECON: ToolsPolicy = {
@@ -406,8 +411,8 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
406
411
  contextMode: "verification",
407
412
  // planning-dispatch: validation is a verification-fan-out unit. It reads
408
413
  // the milestone surface and dispatches reviewer/security/tester subagents
409
- // to report findings without touching user source. Mirrors
410
- // complete-milestone's policy. Write isolation to .gsd/ is preserved.
414
+ // to report findings without touching user source. Write isolation to
415
+ // .gsd/ is preserved.
411
416
  tools: TOOLS_PLANNING_DISPATCH_REVIEW,
412
417
  artifacts: {
413
418
  inline: ["roadmap", "slice-summary", "slice-uat", "requirements", "decisions", "templates"],
@@ -423,11 +428,9 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
423
428
  codebaseMap: false,
424
429
  preferences: "active-only",
425
430
  contextMode: "verification",
426
- // planning-dispatch: completion is a high-leverage place to fan out to
427
- // reviewer / security / tester subagents. They read the diff and report
428
- // findings; they do not write user source. Write isolation to .gsd/ is
429
- // preserved.
430
- tools: TOOLS_PLANNING_DISPATCH_REVIEW,
431
+ // Milestone closeout must run unrestricted shell verification commands
432
+ // against the final diff before recording completion.
433
+ tools: TOOLS_ALL,
431
434
  artifacts: {
432
435
  // #4780 landed slice-summary as excerpt for this unit; phase 2 of
433
436
  // the architecture will read this manifest as the source of truth
@@ -589,7 +592,7 @@ export const UNIT_MANIFESTS: Record<UnitType, UnitContextManifest> = {
589
592
  codebaseMap: false,
590
593
  preferences: "active-only",
591
594
  contextMode: "verification",
592
- tools: TOOLS_PLANNING,
595
+ tools: TOOLS_VERIFICATION,
593
596
  artifacts: {
594
597
  // Phase 3 migration (#4782): manifest matches today's actual
595
598
  // buildRunUatPrompt inlining. Prior phase-1 entry listed
@@ -76,6 +76,13 @@ import {
76
76
  teardownAutoWorktree,
77
77
  } from "./auto-worktree.js";
78
78
 
79
+ const recentWorktreeMergeFailures = new Map<string, number>();
80
+ const MERGE_FAILURE_DEDUPE_MS = 60_000;
81
+
82
+ export function resetRecentWorktreeMergeFailuresForTest(): void {
83
+ recentWorktreeMergeFailures.clear();
84
+ }
85
+
79
86
  // ─── Types ───────────────────────────────────────────────────────────────
80
87
 
81
88
  export interface NotifyCtx {
@@ -836,6 +843,32 @@ function rebuildGitService(
836
843
  s.gitService = deps.gitServiceFactory(s.basePath);
837
844
  }
838
845
 
846
+ function emitWorktreeMergeFailedOnce(
847
+ basePath: string,
848
+ milestoneId: string,
849
+ err: unknown,
850
+ ): void {
851
+ const msg = err instanceof Error ? err.message : String(err);
852
+ const errorCategory = err instanceof Error ? err.name : "Error";
853
+ const now = Date.now();
854
+ const key = `${basePath}\0${milestoneId}\0${errorCategory}`;
855
+ const previous = recentWorktreeMergeFailures.get(key);
856
+ if (previous && now - previous < MERGE_FAILURE_DEDUPE_MS) return;
857
+ for (const [candidate, ts] of recentWorktreeMergeFailures) {
858
+ if (now - ts >= MERGE_FAILURE_DEDUPE_MS) {
859
+ recentWorktreeMergeFailures.delete(candidate);
860
+ }
861
+ }
862
+ emitJournalEvent(basePath, {
863
+ ts: new Date().toISOString(),
864
+ flowId: randomUUID(),
865
+ seq: 0,
866
+ eventType: "worktree-merge-failed",
867
+ data: { milestoneId, error: msg },
868
+ });
869
+ recentWorktreeMergeFailures.set(key, now);
870
+ }
871
+
839
872
  // ─── Session-less merge entry (ADR-016 phase 2 / A1) ─────────────────────
840
873
 
841
874
  /**
@@ -985,13 +1018,7 @@ function _mergeWorktreeModeImpl(
985
1018
  error: msg,
986
1019
  fallback: "chdir-to-project-root",
987
1020
  });
988
- emitJournalEvent(originalBasePath || worktreeBasePath, {
989
- ts: new Date().toISOString(),
990
- flowId: randomUUID(),
991
- seq: 0,
992
- eventType: "worktree-merge-failed",
993
- data: { milestoneId, error: msg },
994
- });
1021
+ emitWorktreeMergeFailedOnce(originalBasePath || worktreeBasePath, milestoneId, err);
995
1022
  // Surface a clear, actionable error. Worktree and milestone branch
996
1023
  // are intentionally preserved — nothing has been deleted. User can
997
1024
  // retry /gsd dispatch complete-milestone or merge manually once the