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
@@ -42,6 +42,20 @@ function makeTmpBase(): string {
42
42
  return base;
43
43
  }
44
44
 
45
+ function makeBrokenIsolatedWorktree(): string {
46
+ const root = mkdtempSync(join(tmpdir(), `gsd-test-5848-${randomUUID().slice(0, 8)}-`));
47
+ tmpDirs.push(root);
48
+ const base = join(root, ".gsd", "projects", "project-id", "worktrees", "M003");
49
+ mkdirSync(join(base, ".gsd", "milestones", "M003", "slices", "S03"), { recursive: true });
50
+ return base;
51
+ }
52
+
53
+ function makeBrokenIsolatedWorktreeRevParse(): string {
54
+ const base = makeBrokenIsolatedWorktree();
55
+ mkdirSync(join(base, ".git"));
56
+ return base;
57
+ }
58
+
45
59
  function resetAutoState(): void {
46
60
  _setAutoActiveForTest(false);
47
61
  }
@@ -231,6 +245,108 @@ describe("Test 5 — postUnitPreVerification short-circuits on deterministic err
231
245
  });
232
246
  });
233
247
 
248
+ describe("Test 5b — broken isolated worktree short-circuits artifact retry (#5848)", () => {
249
+ test("pauses with worktree integrity failure instead of setting pendingVerificationRetry", async () => {
250
+ const { postUnitPreVerification } = await import("../auto-post-unit.ts");
251
+
252
+ const base = makeBrokenIsolatedWorktree();
253
+ const s = new AutoSession();
254
+ s.active = true;
255
+ s.basePath = base;
256
+ s.currentUnit = { type: "research-slice", id: "M003/S03", startedAt: Date.now() };
257
+ s.verificationRetryCount.set("research-slice:M003/S03", 2);
258
+
259
+ const notifications: string[] = [];
260
+ let pauseCalled = false;
261
+ const pctx = {
262
+ s,
263
+ ctx: {
264
+ ui: {
265
+ notify: (message: string) => {
266
+ notifications.push(message);
267
+ },
268
+ },
269
+ },
270
+ pi: {},
271
+ buildSnapshotOpts: () => ({}) as any,
272
+ lockBase: () => base,
273
+ stopAuto: async () => {},
274
+ pauseAuto: async () => {
275
+ pauseCalled = true;
276
+ },
277
+ updateProgressWidget: () => {},
278
+ } as any;
279
+
280
+ const result = await postUnitPreVerification(pctx, {
281
+ skipSettleDelay: true,
282
+ skipWorktreeSync: true,
283
+ });
284
+
285
+ assert.strictEqual(result, "dispatched", "worktree integrity failure must pause instead of retrying");
286
+ assert.strictEqual(pauseCalled, true, "pauseAuto must be called for a broken isolated worktree");
287
+ assert.strictEqual(s.pendingVerificationRetry, null, "pendingVerificationRetry must NOT be set");
288
+ assert.strictEqual(s.verificationRetryCount.has("research-slice:M003/S03"), false, "stale retry count must be cleared");
289
+ assert.ok(
290
+ notifications.some((message) => message.includes("Worktree integrity failure") && message.includes(".git missing")),
291
+ `expected worktree integrity notification, got: ${notifications.join("\n")}`,
292
+ );
293
+ assert.ok(
294
+ notifications.every((message) => !message.includes("Artifact verification failed")),
295
+ `must not surface artifact retry messaging, got: ${notifications.join("\n")}`,
296
+ );
297
+ });
298
+
299
+ test("pauses when git rev-parse cannot validate an isolated worktree", async () => {
300
+ const { postUnitPreVerification } = await import("../auto-post-unit.ts");
301
+
302
+ const base = makeBrokenIsolatedWorktreeRevParse();
303
+ const s = new AutoSession();
304
+ s.active = true;
305
+ s.basePath = base;
306
+ s.currentUnit = { type: "research-slice", id: "M003/S03", startedAt: Date.now() };
307
+ s.verificationRetryCount.set("research-slice:M003/S03", 2);
308
+
309
+ const notifications: string[] = [];
310
+ let pauseCalled = false;
311
+ const pctx = {
312
+ s,
313
+ ctx: {
314
+ ui: {
315
+ notify: (message: string) => {
316
+ notifications.push(message);
317
+ },
318
+ },
319
+ },
320
+ pi: {},
321
+ buildSnapshotOpts: () => ({}) as any,
322
+ lockBase: () => base,
323
+ stopAuto: async () => {},
324
+ pauseAuto: async () => {
325
+ pauseCalled = true;
326
+ },
327
+ updateProgressWidget: () => {},
328
+ } as any;
329
+
330
+ const result = await postUnitPreVerification(pctx, {
331
+ skipSettleDelay: true,
332
+ skipWorktreeSync: true,
333
+ });
334
+
335
+ assert.strictEqual(result, "dispatched", "worktree integrity failure must pause instead of retrying");
336
+ assert.strictEqual(pauseCalled, true, "pauseAuto must be called for a broken isolated worktree");
337
+ assert.strictEqual(s.pendingVerificationRetry, null, "pendingVerificationRetry must NOT be set");
338
+ assert.strictEqual(s.verificationRetryCount.has("research-slice:M003/S03"), false, "stale retry count must be cleared");
339
+ assert.ok(
340
+ notifications.some((message) => message.includes("Worktree integrity failure") && message.includes("git rev-parse")),
341
+ `expected git rev-parse worktree integrity notification, got: ${notifications.join("\n")}`,
342
+ );
343
+ assert.ok(
344
+ notifications.every((message) => !message.includes("Artifact verification failed")),
345
+ `must not surface artifact retry messaging, got: ${notifications.join("\n")}`,
346
+ );
347
+ });
348
+ });
349
+
234
350
  // ─── Test 6: Model-quality failures use standard retry path ──────────────────
235
351
 
236
352
  describe("Test 6 — non-deterministic failures use standard retry; tier escalates once (#4973)", () => {
@@ -3,7 +3,11 @@
3
3
  import test from "node:test";
4
4
  import assert from "node:assert/strict";
5
5
 
6
- import { buildStepCompleteMessage, STEP_COMPLETE_FALLBACK_MESSAGE } from "../auto-post-unit.ts";
6
+ import {
7
+ buildStepCompleteMessage,
8
+ shouldReturnStepWizardAfterUnit,
9
+ STEP_COMPLETE_FALLBACK_MESSAGE,
10
+ } from "../auto-post-unit.ts";
7
11
  import type { GSDState } from "../types.ts";
8
12
 
9
13
  function makeState(overrides: Partial<GSDState>): GSDState {
@@ -51,3 +55,10 @@ test("STEP_COMPLETE_FALLBACK_MESSAGE: used when deriveState throws, still points
51
55
  assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/clear/);
52
56
  assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/gsd/);
53
57
  });
58
+
59
+ test("shouldReturnStepWizardAfterUnit: terminal milestone completion continues to merge-back path", () => {
60
+ assert.equal(shouldReturnStepWizardAfterUnit("complete-milestone", "complete"), false);
61
+ assert.equal(shouldReturnStepWizardAfterUnit("complete-milestone", null), false);
62
+ assert.equal(shouldReturnStepWizardAfterUnit("execute-task", "complete"), false);
63
+ assert.equal(shouldReturnStepWizardAfterUnit("execute-task", "executing"), true);
64
+ });
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
- import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder, refreshRecoveryDbForArtifact } from "../auto-recovery.ts";
8
+ import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, diagnoseWorktreeIntegrityFailure, buildLoopRemediationSteps, writeBlockerPlaceholder, refreshRecoveryDbForArtifact } from "../auto-recovery.ts";
9
9
  import { resolveMilestoneFile } from "../paths.ts";
10
10
  import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask, getMilestoneCommitAttributionShas } from "../gsd-db.ts";
11
11
  import { clearParseCache } from "../files.ts";
@@ -141,6 +141,20 @@ test("resolveExpectedArtifactPath returns null for unknown type", () => {
141
141
  }
142
142
  });
143
143
 
144
+ test("diagnoseWorktreeIntegrityFailure reports missing GSD worktree paths only", () => {
145
+ const missingWorktreePath = join(tmpdir(), `gsd-test-${randomUUID()}`, ".gsd", "worktrees", "M001-S01");
146
+ assert.equal(
147
+ diagnoseWorktreeIntegrityFailure(join(tmpdir(), `gsd-test-${randomUUID()}`)),
148
+ null,
149
+ "non-GSD paths should keep falling through to artifact recovery",
150
+ );
151
+ assert.equal(
152
+ diagnoseWorktreeIntegrityFailure(missingWorktreePath),
153
+ `Worktree integrity failure: ${missingWorktreePath} does not exist. Repair or recreate the worktree before retrying.`,
154
+ "missing GSD worktree paths should fail terminally before artifact retry",
155
+ );
156
+ });
157
+
144
158
  test("resolveExpectedArtifactPath returns correct path for all milestone-level types", () => {
145
159
  const base = makeTmpBase();
146
160
  try {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { describe, test, beforeEach } from "node:test";
4
4
  import assert from "node:assert/strict";
5
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
5
+ import { existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { tmpdir } from "node:os";
8
8
  import { execFileSync } from "node:child_process";
@@ -13,6 +13,7 @@ import {
13
13
  _resetAutoWorktreeOriginalBaseForTests,
14
14
  createAutoWorktree,
15
15
  enterAutoWorktree,
16
+ mergeMilestoneToMain,
16
17
  teardownAutoWorktree,
17
18
  } from "../auto-worktree.ts";
18
19
 
@@ -173,4 +174,71 @@ describe("auto-worktree workspace registry", () => {
173
174
  teardownAutoWorktree(dir2, "M020");
174
175
  try { process.chdir(savedCwd); } catch { /* ignore */ }
175
176
  });
177
+
178
+ test("mergeMilestoneToMain cleans up when milestone branch was already regular-merged", (t) => {
179
+ const tempDir = createTempRepo(t);
180
+ const msDir = join(tempDir, ".gsd", "milestones", "M003");
181
+ mkdirSync(msDir, { recursive: true });
182
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M003 Context\n");
183
+ git(["add", "."], tempDir);
184
+ git(["commit", "-m", "add milestone"], tempDir);
185
+
186
+ createAutoWorktree(tempDir, "M003");
187
+ const wtDir = join(tempDir, ".gsd", "worktrees", "M003");
188
+ writeFileSync(join(wtDir, "feature.txt"), "implemented\n");
189
+ git(["add", "feature.txt"], wtDir);
190
+ git(["commit", "-m", "feat: implement M003"], wtDir);
191
+
192
+ process.chdir(tempDir);
193
+ git(["merge", "--no-ff", "milestone/M003", "-m", "merge M003"], tempDir);
194
+
195
+ process.chdir(wtDir);
196
+ const result = mergeMilestoneToMain(tempDir, "M003", "# M003\n- [x] **S01: Done**\n");
197
+
198
+ assert.equal(result.codeFilesChanged, true);
199
+ assert.equal(result.pushed, false);
200
+ assert.equal(result.prCreated, false);
201
+ assert.equal(existsSync(wtDir), false, "worktree directory is removed");
202
+ assert.throws(
203
+ () => git(["rev-parse", "--verify", "milestone/M003"], tempDir),
204
+ /Command failed/,
205
+ "already-merged milestone branch is deleted",
206
+ );
207
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
208
+ });
209
+
210
+ test("mergeMilestoneToMain cleans up already-merged milestone after main advances", (t) => {
211
+ const tempDir = createTempRepo(t);
212
+ const msDir = join(tempDir, ".gsd", "milestones", "M004");
213
+ mkdirSync(msDir, { recursive: true });
214
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M004 Context\n");
215
+ git(["add", "."], tempDir);
216
+ git(["commit", "-m", "add milestone"], tempDir);
217
+
218
+ createAutoWorktree(tempDir, "M004");
219
+ const wtDir = join(tempDir, ".gsd", "worktrees", "M004");
220
+ writeFileSync(join(wtDir, "feature.txt"), "implemented\n");
221
+ git(["add", "feature.txt"], wtDir);
222
+ git(["commit", "-m", "feat: implement M004"], wtDir);
223
+
224
+ process.chdir(tempDir);
225
+ git(["merge", "--no-ff", "milestone/M004", "-m", "merge M004"], tempDir);
226
+ writeFileSync(join(tempDir, "hotfix.txt"), "later main work\n");
227
+ git(["add", "hotfix.txt"], tempDir);
228
+ git(["commit", "-m", "fix: advance main"], tempDir);
229
+
230
+ process.chdir(wtDir);
231
+ const result = mergeMilestoneToMain(tempDir, "M004", "# M004\n- [x] **S01: Done**\n");
232
+
233
+ assert.equal(result.codeFilesChanged, true);
234
+ assert.equal(result.pushed, false);
235
+ assert.equal(result.prCreated, false);
236
+ assert.equal(existsSync(wtDir), false, "worktree directory is removed");
237
+ assert.throws(
238
+ () => git(["rev-parse", "--verify", "milestone/M004"], tempDir),
239
+ /Command failed/,
240
+ "already-merged milestone branch is deleted",
241
+ );
242
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
243
+ });
176
244
  });
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
@@ -10,7 +10,7 @@ import { runDispatch, runPreDispatch } from "../auto/phases.ts";
10
10
  import { AutoSession } from "../auto/session.ts";
11
11
  import { resolveUnitSupervisionTimeouts } from "../auto-timers.ts";
12
12
  import { bootstrapAutoSession } from "../auto-start.ts";
13
- import { postUnitPreVerification } from "../auto-post-unit.ts";
13
+ import { postUnitPostVerification, postUnitPreVerification } from "../auto-post-unit.ts";
14
14
  import { resolveDispatch, setResearchProjectPromptBuilderForTest } from "../auto-dispatch.ts";
15
15
  import { resolveExpectedArtifactPath, verifyExpectedArtifact, writeBlockerPlaceholder } from "../auto-recovery.ts";
16
16
  import { finalizeProjectResearchTimeout } from "../project-research-policy.ts";
@@ -1587,6 +1587,61 @@ test("deep project setup: discuss-milestone question failure pauses instead of a
1587
1587
  }
1588
1588
  });
1589
1589
 
1590
+ test("verified task git closeout failure retries and continues auto-mode", async () => {
1591
+ const base = makeBase();
1592
+ try {
1593
+ execFileSync("git", ["init"], { cwd: base, stdio: "ignore" });
1594
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: base, stdio: "ignore" });
1595
+ execFileSync("git", ["config", "user.name", "Test User"], { cwd: base, stdio: "ignore" });
1596
+ const hookPath = join(base, ".git", "hooks", "pre-commit");
1597
+ writeFileSync(
1598
+ hookPath,
1599
+ [
1600
+ "#!/bin/sh",
1601
+ "count_file=.git/pre-commit-count",
1602
+ "count=0",
1603
+ "if [ -f \"$count_file\" ]; then count=$(cat \"$count_file\"); fi",
1604
+ "count=$((count + 1))",
1605
+ "printf \"%s\" \"$count\" > \"$count_file\"",
1606
+ "echo blocked by test hook >&2",
1607
+ "exit 1",
1608
+ ].join("\n"),
1609
+ );
1610
+ chmodSync(hookPath, 0o755);
1611
+ writeFileSync(join(base, "work.txt"), "changed\n");
1612
+
1613
+ const s = new AutoSession();
1614
+ s.active = true;
1615
+ s.basePath = base;
1616
+ s.originalBasePath = base;
1617
+ s.currentUnit = { type: "execute-task", id: "M001/S01/T01", startedAt: Date.now() };
1618
+
1619
+ let pauseCalled = false;
1620
+ const notifications: Array<{ message: string; severity?: string }> = [];
1621
+ const result = await postUnitPostVerification({
1622
+ s,
1623
+ ctx: { ui: { notify: (message: string, severity?: string) => notifications.push({ message, severity }) } } as any,
1624
+ pi: {} as any,
1625
+ buildSnapshotOpts: () => ({}) as any,
1626
+ lockBase: () => base,
1627
+ stopAuto: async () => {},
1628
+ pauseAuto: async () => { pauseCalled = true; },
1629
+ updateProgressWidget: () => {},
1630
+ });
1631
+
1632
+ assert.equal(result, "continue");
1633
+ assert.equal(pauseCalled, false);
1634
+ assert.equal(s.lastGitActionStatus, "failed");
1635
+ assert.equal(readFileSync(join(base, ".git", "pre-commit-count"), "utf-8"), "3");
1636
+ assert.ok(
1637
+ notifications.some((entry) => entry.severity === "warning" && entry.message.includes("Git commit failed")),
1638
+ "verified task git closeout failure should warn instead of stopping auto-mode",
1639
+ );
1640
+ } finally {
1641
+ rmSync(base, { recursive: true, force: true });
1642
+ }
1643
+ });
1644
+
1590
1645
  test("deep project setup: approval wait wins over deterministic write-gate placeholder", async () => {
1591
1646
  const base = makeBase();
1592
1647
  try {
@@ -118,3 +118,42 @@ describe("complete phase dispatch guard (#5683)", () => {
118
118
  assert.equal(result?.reason, "All milestones complete.");
119
119
  });
120
120
  });
121
+
122
+ describe("complete milestone context recovery guard (#5831)", () => {
123
+ let base = "";
124
+ const executionEntryRule = DISPATCH_RULES.find(
125
+ (candidate) => candidate.name === "execution-entry phase (no context) → discuss-milestone",
126
+ );
127
+ const prePlanningRule = DISPATCH_RULES.find(
128
+ (candidate) => candidate.name === "pre-planning (no context) → discuss-milestone",
129
+ );
130
+ assert.ok(executionEntryRule, "execution-entry missing-context rule should exist");
131
+ assert.ok(prePlanningRule, "pre-planning missing-context rule should exist");
132
+
133
+ afterEach(() => {
134
+ if (base) rmSync(base, { recursive: true, force: true });
135
+ base = "";
136
+ });
137
+
138
+ test("does not discuss a complete execution-entry milestone with no CONTEXT file", async () => {
139
+ base = makeBase();
140
+ const ctx = buildDispatchCtx(base);
141
+ ctx.state.registry = [{ id: "M001", title: "Milestone One", status: "complete" }];
142
+ ctx.state.phase = "completing-milestone";
143
+
144
+ const result = await executionEntryRule.match(ctx);
145
+
146
+ assert.equal(result, null);
147
+ });
148
+
149
+ test("does not discuss a complete pre-planning milestone with no CONTEXT file", async () => {
150
+ base = makeBase();
151
+ const ctx = buildDispatchCtx(base);
152
+ ctx.state.registry = [{ id: "M001", title: "Milestone One", status: "complete" }];
153
+ ctx.state.phase = "pre-planning";
154
+
155
+ const result = await prePlanningRule.match(ctx);
156
+
157
+ assert.equal(result, null);
158
+ });
159
+ });
@@ -141,6 +141,14 @@ test("Feature 1: executive summary paragraph is rendered", () => {
141
141
  assert.ok(html.includes("$2.50 spent"), "should contain cost");
142
142
  });
143
143
 
144
+ test("report uses the shared GSD HTML shell", () => {
145
+ const html = generateHtmlReport(mockData(), mockOpts());
146
+ assert.ok(html.includes('<span class="logo">GSD</span>'), "should render shared shell logo");
147
+ assert.ok(html.includes('<span class="kind-chip">Report</span>'), "should render report kind chip");
148
+ assert.ok(html.includes('<nav class="toc" aria-label="Report sections">'), "should render shared shell TOC");
149
+ assert.ok(html.includes('<main>'), "should render content inside shared shell main");
150
+ });
151
+
144
152
  test("Feature 1: executive summary includes budget context when set", () => {
145
153
  const data = mockData({ health: { ...mockData().health, budgetCeiling: 10.00 } });
146
154
  const html = generateHtmlReport(data, mockOpts());
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { test } from "node:test";
9
9
  import assert from "node:assert/strict";
10
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
10
+ import { existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { tmpdir } from "node:os";
13
13
 
@@ -48,6 +48,60 @@ test("parkMilestone updates DB status to 'parked' (#2694)", () => {
48
48
  }
49
49
  });
50
50
 
51
+ test("parkMilestone ignores blocked SUMMARY.md when DB milestone is active (#5828)", () => {
52
+ const base = createBase();
53
+ try {
54
+ openDatabase(":memory:");
55
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
56
+ writeFileSync(
57
+ join(base, ".gsd", "milestones", "M001", "M001-SUMMARY.md"),
58
+ [
59
+ "---",
60
+ "status: closeout_blocked",
61
+ "---",
62
+ "",
63
+ "# M001 Summary",
64
+ "",
65
+ "Completion was not persisted.",
66
+ ].join("\n"),
67
+ "utf-8",
68
+ );
69
+
70
+ const parked = parkMilestone(base, "M001", "test");
71
+
72
+ assert.ok(parked, "active DB row should allow parking despite a blocked SUMMARY.md");
73
+ assert.ok(
74
+ existsSync(join(base, ".gsd", "milestones", "M001", "M001-PARKED.md")),
75
+ "PARKED.md should be written",
76
+ );
77
+ assert.equal(getMilestone("M001")!.status, "parked", "DB status should be parked");
78
+ } finally {
79
+ closeDatabase();
80
+ rmSync(base, { recursive: true, force: true });
81
+ }
82
+ });
83
+
84
+ test("parkMilestone refuses DB-complete milestones (#5828)", () => {
85
+ const base = createBase();
86
+ try {
87
+ openDatabase(":memory:");
88
+ insertMilestone({ id: "M001", title: "Test", status: "complete" });
89
+
90
+ const parked = parkMilestone(base, "M001", "test");
91
+
92
+ assert.equal(parked, false, "complete DB row should not be parkable");
93
+ assert.equal(
94
+ existsSync(join(base, ".gsd", "milestones", "M001", "M001-PARKED.md")),
95
+ false,
96
+ "PARKED.md should not be written",
97
+ );
98
+ assert.equal(getMilestone("M001")!.status, "complete", "DB status should remain complete");
99
+ } finally {
100
+ closeDatabase();
101
+ rmSync(base, { recursive: true, force: true });
102
+ }
103
+ });
104
+
51
105
  test("unparkMilestone updates DB status to 'active' (#2694)", () => {
52
106
  const base = createBase();
53
107
  try {
@@ -125,6 +125,31 @@ test('handlePlanSlice advances DB-derived state out of planning immediately', as
125
125
  }
126
126
  });
127
127
 
128
+ test('handlePlanSlice clears sketch flag so DB-derived state leaves refining', async () => {
129
+ const base = makeTmpBase();
130
+ openDatabase(join(base, '.gsd', 'gsd.db'));
131
+
132
+ try {
133
+ insertMilestone({ id: 'M001', title: 'Milestone', status: 'active' });
134
+ insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Planning slice', status: 'pending', demo: 'Rendered plans exist.', isSketch: true });
135
+
136
+ invalidateStateCache();
137
+ const before = await deriveState(base);
138
+ assert.equal(before.phase, 'refining');
139
+
140
+ const result = await handlePlanSlice(validParams(), base);
141
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
142
+ assert.equal(getSlice('M001', 'S02')?.is_sketch, 0, 'planned slice must no longer be treated as a sketch');
143
+
144
+ invalidateStateCache();
145
+ const after = await deriveState(base);
146
+ assert.notEqual(after.phase, 'refining');
147
+ assert.equal(after.progress?.tasks?.total, 2);
148
+ } finally {
149
+ cleanup(base);
150
+ }
151
+ });
152
+
128
153
  test('handlePlanSlice leaves omitted enrichment fields empty instead of rendering placeholders', async () => {
129
154
  const base = makeTmpBase();
130
155
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -11,7 +11,7 @@ const source = readFileSync(
11
11
 
12
12
  test("postUnitPreVerification blocks on git action failure", () => {
13
13
  const failureBlock = extractSourceRegion(source, 'if (gitResult.status === "failed")');
14
- assert.ok(failureBlock.includes('ctx.ui.notify(failureMsg, "error")'));
14
+ assert.ok(failureBlock.includes('ctx.ui.notify(failureMsg, opts?.softFailure ? "warning" : "error")'));
15
15
  assert.ok(failureBlock.includes("await pauseAuto(ctx, pi)"));
16
16
  assert.ok(failureBlock.includes('return "dispatched"'));
17
17
  assert.ok(!failureBlock.includes("git-action-failed-nonblocking"));
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Regression test for #2675: completing-milestone dispatch rule must
3
- * block completion when VALIDATION verdict is "needs-remediation".
2
+ * Regression tests for non-passing VALIDATION verdicts: completing-milestone
3
+ * dispatch must block completion when VALIDATION needs remediation or attention.
4
4
  *
5
5
  * Without this guard, needs-remediation + allSlicesDone causes a loop:
6
6
  * complete-milestone dispatched → agent refuses (correct) → no SUMMARY
@@ -66,6 +66,50 @@ test("completing-milestone blocks when VALIDATION verdict is needs-remediation (
66
66
  }
67
67
  });
68
68
 
69
+ test("completing-milestone blocks when VALIDATION verdict is needs-attention (#5747)", async () => {
70
+ const base = mkdtempSync(join(tmpdir(), "gsd-attention-"));
71
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
72
+
73
+ try {
74
+ writeFileSync(
75
+ join(base, ".gsd", "milestones", "M001", "M001-VALIDATION.md"),
76
+ [
77
+ "---",
78
+ "verdict: needs-attention",
79
+ "remediation_round: 0",
80
+ "---",
81
+ "",
82
+ "# Validation Report",
83
+ "",
84
+ "Acceptance proof is incomplete and needs human attention.",
85
+ ].join("\n"),
86
+ );
87
+
88
+ const ctx = {
89
+ mid: "M001",
90
+ midTitle: "Test Milestone",
91
+ basePath: base,
92
+ state: { phase: "completing-milestone" } as any,
93
+ prefs: {} as any,
94
+ session: undefined,
95
+ };
96
+
97
+ const result = await completingRule!.match(ctx);
98
+
99
+ assert.ok(result !== null, "rule should match");
100
+ assert.equal(result!.action, "stop", "should return stop action");
101
+ if (result!.action === "stop") {
102
+ assert.equal(result!.level, "warning", "should be warning level (pausable)");
103
+ assert.ok(
104
+ result!.reason.includes("needs-attention"),
105
+ "reason should mention needs-attention",
106
+ );
107
+ }
108
+ } finally {
109
+ rmSync(base, { recursive: true, force: true });
110
+ }
111
+ });
112
+
69
113
  test("completing-milestone proceeds normally when VALIDATION verdict is pass (#2675 guard)", async () => {
70
114
  const base = mkdtempSync(join(tmpdir(), "gsd-remediation-"));
71
115
  mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
@@ -5,6 +5,7 @@ import test from "node:test";
5
5
  import assert from "node:assert/strict";
6
6
 
7
7
  import {
8
+ _hasEmptyAgentEndContent,
8
9
  _handleSessionSwitchAgentEnd,
9
10
  isBareClaudeCodeStreamAbortPlaceholder,
10
11
  isClaudeCodeSessionSwitchAbortMessage,
@@ -188,6 +189,15 @@ test("empty-content aborted during session-switch is silently ignored", () => {
188
189
  assert.equal(cancelledWith, null);
189
190
  });
190
191
 
192
+ test("missing agent_end content is classified as empty abort content", () => {
193
+ // Providers may omit content entirely for a late aborted agent_end. That is
194
+ // equivalent to empty content and must not pause/cancel the next unit.
195
+ assert.equal(_hasEmptyAgentEndContent(undefined), true);
196
+ assert.equal(_hasEmptyAgentEndContent(null), true);
197
+ assert.equal(_hasEmptyAgentEndContent([]), true);
198
+ assert.equal(_hasEmptyAgentEndContent([{ type: "text", text: "partial" }]), false);
199
+ });
200
+
191
201
  test("completed assistant content with aborted stopReason during session-switch is ignored", () => {
192
202
  // newSession() can abort the just-finished provider stream while the last
193
203
  // assistant message still carries the completed unit summary. That is a
@@ -23,9 +23,12 @@ import { registerAutoWorker } from "../db/auto-workers.ts";
23
23
  import { claimMilestoneLease } from "../db/milestone-leases.ts";
24
24
  import {
25
25
  recordDispatchClaim,
26
+ markCanceled,
26
27
  getRecentUnitKeysForWorker,
28
+ getRecentUnitKeysForProjectRoot,
27
29
  } from "../db/unit-dispatches.ts";
28
30
  import { setRuntimeKv, getRuntimeKv } from "../db/runtime-kv.ts";
31
+ import { detectStuck } from "../auto/detect-stuck.ts";
29
32
 
30
33
  function makeBase(): string {
31
34
  const base = mkdtempSync(join(tmpdir(), "gsd-stuck-state-db-"));
@@ -67,6 +70,42 @@ test("getRecentUnitKeysForWorker reconstructs the recentUnits sliding window", (
67
70
  assert.deepEqual(window.map(w => w.key), ["U1", "U2", "U3"]);
68
71
  });
69
72
 
73
+ test("getRecentUnitKeysForProjectRoot restores compound keys used by stuck detection", (t) => {
74
+ const base = makeBase();
75
+ t.after(() => cleanup(base));
76
+ openDatabase(join(base, ".gsd", "gsd.db"));
77
+ insertMilestone({ id: "M001", title: "T", status: "active" });
78
+ const worker = registerAutoWorker({ projectRootRealpath: base });
79
+ const lease = claimMilestoneLease(worker, "M001");
80
+ assert.equal(lease.ok, true);
81
+ if (!lease.ok) return;
82
+
83
+ for (let i = 0; i < 2; i++) {
84
+ const claim = recordDispatchClaim({
85
+ traceId: `t${i}`,
86
+ workerId: worker,
87
+ milestoneLeaseToken: lease.token,
88
+ milestoneId: "M001",
89
+ sliceId: "S01",
90
+ unitType: "complete-slice",
91
+ unitId: "M001/S01",
92
+ });
93
+ assert.equal(claim.ok, true);
94
+ if (!claim.ok) return;
95
+ markCanceled(claim.dispatchId, "pause");
96
+ }
97
+
98
+ const window = getRecentUnitKeysForProjectRoot(base, 20);
99
+ assert.deepEqual(window.map(w => w.key), [
100
+ "complete-slice/M001/S01",
101
+ "complete-slice/M001/S01",
102
+ ]);
103
+
104
+ const result = detectStuck([...window, { key: "complete-slice/M001/S01" }]);
105
+ assert.equal(result?.stuck, true);
106
+ assert.match(result?.reason ?? "", /3 consecutive times/);
107
+ });
108
+
70
109
  test("getRecentUnitKeysForWorker honors the limit parameter", (t) => {
71
110
  const base = makeBase();
72
111
  t.after(() => cleanup(base));