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
@@ -1 +1 @@
1
- 0829fe9060dc3f75
1
+ ed49f911008c62ca
@@ -105,9 +105,10 @@ export function mapUsage(sdkUsage, totalCostUsd) {
105
105
  output: sdkUsage.output_tokens,
106
106
  cacheRead: sdkUsage.cache_read_input_tokens,
107
107
  cacheWrite: sdkUsage.cache_creation_input_tokens,
108
+ // Claude Agent SDK result usage is cumulative across its internal loop;
109
+ // repeated cache reads do not represent additional live context.
108
110
  totalTokens: sdkUsage.input_tokens +
109
111
  sdkUsage.output_tokens +
110
- sdkUsage.cache_read_input_tokens +
111
112
  sdkUsage.cache_creation_input_tokens,
112
113
  cost: {
113
114
  input: 0,
@@ -102,6 +102,9 @@ function missingSliceStop(mid, phase) {
102
102
  level: "error",
103
103
  };
104
104
  }
105
+ function isRegistryMilestoneComplete(state, mid) {
106
+ return state.registry.some((milestone) => milestone.id === mid && milestone.status === "complete");
107
+ }
105
108
  /**
106
109
  * Check for milestone slices missing SUMMARY files.
107
110
  * Returns array of missing slice IDs, or empty array if all present or DB unavailable.
@@ -247,6 +250,8 @@ export const DISPATCH_RULES = [
247
250
  return null;
248
251
  if (!MILESTONE_ID_RE.test(mid))
249
252
  return null;
253
+ if (isRegistryMilestoneComplete(state, mid))
254
+ return null;
250
255
  // Align with the plan-v2 gate's lookup semantics: whitespace-only counts
251
256
  // as missing, and an auto worktree may fall back to GSD_PROJECT_ROOT.
252
257
  if (hasFinalizedMilestoneContext(basePath, mid))
@@ -557,6 +562,8 @@ export const DISPATCH_RULES = [
557
562
  match: async ({ state, mid, midTitle, basePath, prefs, structuredQuestionsAvailable }) => {
558
563
  if (state.phase !== "pre-planning")
559
564
  return null;
565
+ if (isRegistryMilestoneComplete(state, mid))
566
+ return null;
560
567
  const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
561
568
  const hasContext = !!(contextFile && (await loadFile(contextFile)));
562
569
  if (hasContext)
@@ -1091,19 +1098,19 @@ export const DISPATCH_RULES = [
1091
1098
  return { action: "skip" };
1092
1099
  }
1093
1100
  }
1094
- // Safety guard (#2675): block completion when VALIDATION verdict is
1095
- // needs-remediation. The state machine treats needs-remediation as
1096
- // terminal (to prevent validate-milestone loops per #832), but
1097
- // completing-milestone should NOT proceed — remediation work is needed.
1101
+ // Safety guard (#2675, #5747): block completion when VALIDATION
1102
+ // verdict is non-passing. The state machine treats these verdicts as
1103
+ // terminal, but completing-milestone should NOT proceed — remediation
1104
+ // or human attention is needed.
1098
1105
  const validationFile = resolveMilestoneFile(basePath, mid, "VALIDATION");
1099
1106
  if (validationFile) {
1100
1107
  const validationContent = await loadFile(validationFile);
1101
1108
  if (validationContent) {
1102
1109
  const verdict = extractVerdict(validationContent);
1103
- if (verdict === "needs-remediation") {
1110
+ if (verdict === "needs-remediation" || verdict === "needs-attention") {
1104
1111
  return {
1105
1112
  action: "stop",
1106
- reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "needs-remediation". Address the remediation findings and re-run validation, or update the verdict manually.`,
1113
+ reason: `Cannot complete milestone ${mid}: VALIDATION verdict is "${verdict}". Address the validation findings and re-run validation, or update the verdict manually.`,
1107
1114
  level: "warning",
1108
1115
  };
1109
1116
  }
@@ -23,7 +23,7 @@ import { rebuildState } from "./doctor.js";
23
23
  import { parseUnitId } from "./unit-id.js";
24
24
  import { closeoutUnit } from "./auto-unit-closeout.js";
25
25
  import { runTurnGitAction, } from "./git-service.js";
26
- import { verifyExpectedArtifact, resolveExpectedArtifactPath, writeBlockerPlaceholder, diagnoseExpectedArtifact, } from "./auto-recovery.js";
26
+ import { verifyExpectedArtifact, resolveExpectedArtifactPath, writeBlockerPlaceholder, diagnoseExpectedArtifact, diagnoseWorktreeIntegrityFailure, } from "./auto-recovery.js";
27
27
  import { regenerateIfMissing } from "./workflow-projections.js";
28
28
  import { WorktreeStateProjection } from "./worktree-state-projection.js";
29
29
  import { createWorkspace, scopeMilestone } from "./workspace.js";
@@ -303,6 +303,19 @@ export function buildStepCompleteMessage(nextState) {
303
303
  return `Step complete. Next: ${next.label}\n`
304
304
  + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
305
305
  }
306
+ /**
307
+ * Decide whether step mode should stop at the step wizard after a unit finishes.
308
+ *
309
+ * @param currentUnitType The just-finished unit type, such as "execute-task" or
310
+ * "complete-milestone"; may be null/undefined when no current unit is known.
311
+ * @param phaseAfterUnit The freshly derived next phase, such as "executing" or
312
+ * "complete"; may be null/undefined if state derivation failed.
313
+ * @returns true to show the step wizard; false to keep the loop running so
314
+ * terminal milestone completion can reach the merge/finalization path.
315
+ */
316
+ export function shouldReturnStepWizardAfterUnit(currentUnitType, phaseAfterUnit) {
317
+ return currentUnitType !== "complete-milestone" && phaseAfterUnit !== "complete";
318
+ }
306
319
  export const USER_DRIVEN_DEEP_UNITS = new Set([
307
320
  "discuss-project",
308
321
  "discuss-requirements",
@@ -318,6 +331,10 @@ function artifactValidationKind(unitType) {
318
331
  return null;
319
332
  }
320
333
  function describeArtifactVerificationFailure(unitType, unitId, basePath) {
334
+ const worktreeFailure = diagnoseWorktreeIntegrityFailure(basePath);
335
+ if (worktreeFailure) {
336
+ return `${worktreeFailure} Unit: ${unitType} ${unitId}.`;
337
+ }
321
338
  const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
322
339
  if (!artifactPath) {
323
340
  return `Artifact verification failed: ${unitType} "${unitId}" has no resolvable artifact path.`;
@@ -362,7 +379,14 @@ export async function autoCommitUnit(basePath, unitType, unitId, ctx) {
362
379
  return null;
363
380
  }
364
381
  }
365
- async function runCloseoutGitAction(pctx, unit) {
382
+ /**
383
+ * Execute the turn-level git action (commit, snapshot, or status-only).
384
+ *
385
+ * @param opts.softFailure - Defaults to false. When true, retry git failures,
386
+ * warn, and continue without pausing auto-mode; use for best-effort deferred
387
+ * closeout work where a git failure should not block the run.
388
+ */
389
+ async function runCloseoutGitAction(pctx, unit, opts) {
366
390
  const { s, ctx, pi, pauseAuto } = pctx;
367
391
  const prefs = loadEffectiveGSDPreferences()?.preferences;
368
392
  const uokFlags = resolveUokFlags(prefs);
@@ -390,13 +414,24 @@ async function runCloseoutGitAction(pctx, unit) {
390
414
  });
391
415
  }
392
416
  else {
393
- const gitResult = runTurnGitAction({
417
+ const maxAttempts = opts?.softFailure ? 3 : 1;
418
+ let gitResult = runTurnGitAction({
394
419
  basePath: s.basePath,
395
420
  action: turnAction,
396
421
  unitType: unit.type,
397
422
  unitId: unit.id,
398
423
  taskContext,
399
424
  });
425
+ for (let attempt = 1; gitResult.status === "failed" && attempt < maxAttempts; attempt++) {
426
+ await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
427
+ gitResult = runTurnGitAction({
428
+ basePath: s.basePath,
429
+ action: turnAction,
430
+ unitType: unit.type,
431
+ unitId: unit.id,
432
+ taskContext,
433
+ });
434
+ }
400
435
  if (uokFlags.gitops) {
401
436
  writeTurnGitTransaction({
402
437
  basePath: s.basePath,
@@ -444,12 +479,15 @@ async function runCloseoutGitAction(pctx, unit) {
444
479
  });
445
480
  }
446
481
  const failureMsg = `Git ${turnAction} failed: ${(gitResult.error ?? "unknown error").split("\n")[0]}`;
447
- ctx.ui.notify(failureMsg, "error");
482
+ ctx.ui.notify(failureMsg, opts?.softFailure ? "warning" : "error");
448
483
  debugLog("postUnit", {
449
- phase: "git-action-failed-blocking",
484
+ phase: opts?.softFailure ? "git-action-failed-soft" : "git-action-failed-blocking",
450
485
  action: turnAction,
451
486
  error: gitResult.error ?? "unknown error",
452
487
  });
488
+ if (opts?.softFailure) {
489
+ return "continue";
490
+ }
453
491
  await pauseAuto(ctx, pi);
454
492
  return "dispatched";
455
493
  }
@@ -467,7 +505,10 @@ async function runCloseoutGitAction(pctx, unit) {
467
505
  s.lastGitActionFailure = message;
468
506
  s.lastGitActionStatus = "failed";
469
507
  debugLog("postUnit", { phase: "git-action", error: message, action: turnAction });
470
- ctx.ui.notify(`Git ${turnAction} failed: ${message.split("\n")[0]}`, uokFlags.gitops ? "error" : "warning");
508
+ ctx.ui.notify(`Git ${turnAction} failed: ${message.split("\n")[0]}`, opts?.softFailure ? "warning" : "error");
509
+ if (opts?.softFailure) {
510
+ return "continue";
511
+ }
471
512
  if (uokFlags.gitops) {
472
513
  await pauseAuto(ctx, pi);
473
514
  return "dispatched";
@@ -961,6 +1002,22 @@ export async function postUnitPreVerification(pctx, opts) {
961
1002
  ctx.ui.notify(`${s.currentUnit.type} ${s.currentUnit.id} — deterministic policy rejection, wrote blocker placeholder (no retries) (#4973)`, "warning");
962
1003
  // Fall through to "continue" — do NOT enter the retry or db-unavailable paths.
963
1004
  }
1005
+ else if (!triggerArtifactVerified && diagnoseWorktreeIntegrityFailure(s.basePath)) {
1006
+ const retryKey = `${s.currentUnit.type}:${s.currentUnit.id}`;
1007
+ const worktreeFailure = diagnoseWorktreeIntegrityFailure(s.basePath);
1008
+ s.pendingVerificationRetry = null;
1009
+ s.verificationRetryCount.delete(retryKey);
1010
+ s.verificationRetryFailureHashes.delete(retryKey);
1011
+ debugLog("postUnit", {
1012
+ phase: "worktree-integrity-failure",
1013
+ unitType: s.currentUnit.type,
1014
+ unitId: s.currentUnit.id,
1015
+ basePath: s.basePath,
1016
+ });
1017
+ ctx.ui.notify(`${worktreeFailure} Retry ${s.currentUnit.id} after repair.`, "error");
1018
+ await pauseAuto(ctx, pi);
1019
+ return "dispatched";
1020
+ }
964
1021
  else if (!triggerArtifactVerified && !isDbAvailable()) {
965
1022
  debugLog("postUnit", { phase: "artifact-verify-skip-db-unavailable", unitType: s.currentUnit.type, unitId: s.currentUnit.id });
966
1023
  const dbSkipDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
@@ -1032,7 +1089,7 @@ export async function postUnitPostVerification(pctx) {
1032
1089
  const { s, ctx, pi, buildSnapshotOpts, lockBase, stopAuto, pauseAuto, updateProgressWidget } = pctx;
1033
1090
  if (s.currentUnit) {
1034
1091
  if (shouldDeferCloseoutGitAction(s.currentUnit.type)) {
1035
- const gitActionResult = await runCloseoutGitAction(pctx, s.currentUnit);
1092
+ const gitActionResult = await runCloseoutGitAction(pctx, s.currentUnit, { softFailure: true });
1036
1093
  if (gitActionResult === "dispatched") {
1037
1094
  return "stopped";
1038
1095
  }
@@ -1404,15 +1461,19 @@ export async function postUnitPostVerification(pctx) {
1404
1461
  // Without this notify(), /gsd in step mode finishes a unit and silently
1405
1462
  // exits the loop, leaving the user with no hint to /clear and /gsd again.
1406
1463
  if (s.stepMode) {
1464
+ let phaseAfterUnit = null;
1407
1465
  try {
1408
1466
  const nextState = await deriveState(s.canonicalProjectRoot);
1467
+ phaseAfterUnit = nextState.phase;
1409
1468
  ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
1410
1469
  }
1411
1470
  catch (e) {
1412
1471
  debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
1413
1472
  ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
1414
1473
  }
1415
- return "step-wizard";
1474
+ return shouldReturnStepWizardAfterUnit(s.currentUnit?.type, phaseAfterUnit)
1475
+ ? "step-wizard"
1476
+ : "continue";
1416
1477
  }
1417
1478
  return "continue";
1418
1479
  }
@@ -14,7 +14,8 @@ import { clearParseCache } from "./files.js";
14
14
  import { parseRoadmap as parseLegacyRoadmap, parsePlan as parseLegacyPlan } from "./parsers-legacy.js";
15
15
  import { isDbAvailable, getTask, getSlice, getSliceTasks, getPendingGates, updateTaskStatus, updateSliceStatus, insertSlice, getMilestone, refreshOpenDatabaseFromDisk, getCompletedMilestoneTaskFileHints, getMilestoneCommitAttributionShas, recordMilestoneCommitAttribution } from "./gsd-db.js";
16
16
  import { isValidationTerminal } from "./state.js";
17
- import { logWarning } from "./workflow-logger.js";
17
+ import { getErrorMessage } from "./error-utils.js";
18
+ import { logWarning, logError } from "./workflow-logger.js";
18
19
  import { readIntegrationBranch } from "./git-service.js";
19
20
  import { isClosedStatus } from "./status-guards.js";
20
21
  import { resolveSlicePath, resolveSliceFile, resolveTasksDir, resolveTaskFiles, relMilestoneFile, relSliceFile, buildSliceFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
@@ -25,9 +26,33 @@ import { resolveExpectedArtifactPath, diagnoseExpectedArtifact, } from "./auto-a
25
26
  import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.js";
26
27
  import { validateArtifact } from "./schemas/validate.js";
27
28
  import { getProjectResearchStatus } from "./project-research-policy.js";
29
+ import { isGsdWorktreePath } from "./worktree-root.js";
28
30
  // Re-export so existing consumers of auto-recovery.ts keep working.
29
31
  export { resolveExpectedArtifactPath, diagnoseExpectedArtifact };
30
32
  export { classifyMilestoneSummaryContent, } from "./milestone-summary-classifier.js";
33
+ // ─── Artifact Resolution & Verification ───────────────────────────────────────
34
+ export function diagnoseWorktreeIntegrityFailure(basePath) {
35
+ if (!isGsdWorktreePath(basePath))
36
+ return null;
37
+ if (!existsSync(basePath)) {
38
+ return `Worktree integrity failure: ${basePath} does not exist. Repair or recreate the worktree before retrying.`;
39
+ }
40
+ const gitPath = join(basePath, ".git");
41
+ if (!existsSync(gitPath)) {
42
+ return `Worktree integrity failure: ${basePath} is not a valid git worktree (.git missing). Repair or recreate the worktree before retrying.`;
43
+ }
44
+ try {
45
+ execFileSync("git", ["rev-parse", "--git-dir"], {
46
+ cwd: basePath,
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ encoding: "utf-8",
49
+ });
50
+ return null;
51
+ }
52
+ catch (err) {
53
+ return `Worktree integrity failure: ${basePath} is not a valid git worktree (git rev-parse failed: ${getErrorMessage(err).split("\n")[0]}). Repair or recreate the worktree before retrying.`;
54
+ }
55
+ }
31
56
  export function refreshRecoveryDbForArtifact(unitType, unitId) {
32
57
  if (unitType !== "plan-slice" && unitType !== "execute-task")
33
58
  return { ok: true };
@@ -673,6 +698,11 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
673
698
  return false;
674
699
  }
675
700
  if (!existsSync(absPath)) {
701
+ const worktreeFailure = diagnoseWorktreeIntegrityFailure(base);
702
+ if (worktreeFailure) {
703
+ logError("recovery", `${worktreeFailure} Unit: ${unitType} ${unitId}.`);
704
+ return false;
705
+ }
676
706
  logWarning("recovery", `verify-fail ${unitType} ${unitId}: existsSync false for ${absPath}`);
677
707
  return false;
678
708
  }
@@ -606,12 +606,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
606
606
  // worktree cleanup) was never run — the survivor branch must be merged.
607
607
  // Applies to both worktree and branch isolation modes.
608
608
  let hasSurvivorBranch = false;
609
- if (state.activeMilestone &&
609
+ let survivorMilestoneId = state.activeMilestone?.id ?? null;
610
+ if (!survivorMilestoneId && state.phase === "complete") {
611
+ survivorMilestoneId = findUnmergedCompletedMilestone(base, getIsolationMode(base));
612
+ }
613
+ if (survivorMilestoneId &&
610
614
  (state.phase === "pre-planning" || state.phase === "complete") &&
611
615
  getIsolationMode(base) !== "none" &&
612
616
  !detectWorktreeName(base) &&
613
617
  !base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)) {
614
- const milestoneBranch = `milestone/${state.activeMilestone.id}`;
618
+ const milestoneBranch = `milestone/${survivorMilestoneId}`;
615
619
  const { nativeBranchExists } = await import("./native-git-bridge.js");
616
620
  hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
617
621
  if (hasSurvivorBranch) {
@@ -645,7 +649,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
645
649
  // Re-evaluate via the helper — the discuss branch above may have cleared
646
650
  // hasSurvivorBranch after a successful promotion.
647
651
  if (decideSurvivorAction(hasSurvivorBranch, state.phase) === "finalize") {
648
- const mid = state.activeMilestone.id;
652
+ const mid = survivorMilestoneId;
649
653
  // Commit 68ef58a3c made `_mergeBranchMode` throw on wrong-branch
650
654
  // instead of returning false silently. Wrap the call so the throw is
651
655
  // converted into an error notify + clean bootstrap abort, not an
@@ -204,6 +204,52 @@ function gitRemoteExists(basePath, remote) {
204
204
  return false;
205
205
  }
206
206
  }
207
+ function findRegularMergeChangedPaths(basePath, milestoneBranch, mainBranch) {
208
+ const changedPaths = new Set();
209
+ let mergeLog = "";
210
+ try {
211
+ mergeLog = execFileSync("git", ["rev-list", "--merges", "--parents", mainBranch], {
212
+ cwd: basePath,
213
+ stdio: ["ignore", "pipe", "pipe"],
214
+ encoding: "utf-8",
215
+ }).trim();
216
+ }
217
+ catch (err) {
218
+ logWarning("worktree", `regular merge lookup failed: ${err instanceof Error ? err.message : String(err)}`);
219
+ return changedPaths;
220
+ }
221
+ for (const line of mergeLog.split("\n").filter(Boolean)) {
222
+ const [mergeCommit, firstParent, ...otherParents] = line.split(" ");
223
+ if (!mergeCommit || !firstParent || otherParents.length === 0)
224
+ continue;
225
+ const mergedMilestone = otherParents.some((parent) => {
226
+ try {
227
+ return nativeIsAncestor(basePath, milestoneBranch, parent);
228
+ }
229
+ catch {
230
+ return false;
231
+ }
232
+ });
233
+ if (!mergedMilestone)
234
+ continue;
235
+ try {
236
+ const output = execFileSync("git", ["diff", "--name-only", firstParent, mergeCommit], {
237
+ cwd: basePath,
238
+ stdio: ["ignore", "pipe", "pipe"],
239
+ encoding: "utf-8",
240
+ }).trim();
241
+ for (const path of output.split("\n").filter(Boolean)) {
242
+ if (!path.startsWith(".gsd/"))
243
+ changedPaths.add(path);
244
+ }
245
+ }
246
+ catch (err) {
247
+ logWarning("worktree", `regular merge diff lookup failed: ${err instanceof Error ? err.message : String(err)}`);
248
+ }
249
+ return changedPaths;
250
+ }
251
+ return changedPaths;
252
+ }
207
253
  function clearProjectRootStateFiles(basePath, milestoneId) {
208
254
  const gsdDir = gsdRoot(basePath);
209
255
  // Phase C pt 2: auto.lock removed from this list — the file is gone
@@ -1482,6 +1528,56 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1482
1528
  });
1483
1529
  }
1484
1530
  }
1531
+ // Already regular-merged milestones can skip the squash path and proceed to cleanup (#5831).
1532
+ if (nativeIsAncestor(originalBasePath_, milestoneBranch, mainBranch)) {
1533
+ const codeChanges = nativeDiffNumstat(originalBasePath_, mainBranch, milestoneBranch).filter((entry) => !entry.path.startsWith(".gsd/"));
1534
+ if (codeChanges.length > 0) {
1535
+ const regularMergeChangedPaths = findRegularMergeChangedPaths(originalBasePath_, milestoneBranch, mainBranch);
1536
+ const unanchoredCodeChanges = codeChanges.filter((entry) => regularMergeChangedPaths.has(entry.path));
1537
+ if (unanchoredCodeChanges.length > 0) {
1538
+ process.chdir(previousCwd);
1539
+ throw new GSDError(GSD_GIT_ERROR, `Milestone branch "${milestoneBranch}" is reachable from "${mainBranch}" ` +
1540
+ `but has ${unanchoredCodeChanges.length} milestone-touched code file(s) not on current "${mainBranch}". ` +
1541
+ `Aborting worktree teardown to prevent data loss.`);
1542
+ }
1543
+ }
1544
+ debugLog("mergeMilestoneToMain", {
1545
+ action: "skip-squash-already-merged",
1546
+ milestoneId,
1547
+ milestoneBranch,
1548
+ mainBranch,
1549
+ });
1550
+ try {
1551
+ clearProjectRootStateFiles(originalBasePath_, milestoneId);
1552
+ }
1553
+ catch (err) {
1554
+ logWarning("worktree", `clearProjectRootStateFiles failed during already-merged cleanup: ${err instanceof Error ? err.message : String(err)}`);
1555
+ }
1556
+ try {
1557
+ removeWorktree(originalBasePath_, milestoneId, {
1558
+ branch: milestoneBranch,
1559
+ deleteBranch: false,
1560
+ });
1561
+ }
1562
+ catch (err) {
1563
+ logWarning("worktree", `worktree removal failed: ${err instanceof Error ? err.message : String(err)}`);
1564
+ }
1565
+ try {
1566
+ nativeBranchDelete(originalBasePath_, milestoneBranch);
1567
+ }
1568
+ catch (err) {
1569
+ logWarning("worktree", `git branch-delete failed: ${err instanceof Error ? err.message : String(err)}`);
1570
+ }
1571
+ setActiveWorkspace(null);
1572
+ nudgeGitBranchCache(previousCwd);
1573
+ try {
1574
+ process.chdir(originalBasePath_);
1575
+ }
1576
+ catch (err) {
1577
+ logWarning("worktree", `chdir to project root after already-merged cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
1578
+ }
1579
+ return { commitMessage, pushed: false, prCreated: false, codeFilesChanged: true };
1580
+ }
1485
1581
  // 7. Shelter queued milestone directories before the squash merge (#2505).
1486
1582
  // The milestone branch may contain copies of queued milestone dirs (via
1487
1583
  // copyPlanningArtifacts), so `git merge --squash` rejects when those same
@@ -18,6 +18,9 @@ const MAX_NETWORK_RETRIES = 2;
18
18
  function isObjectRecord(value) {
19
19
  return !!value && typeof value === "object";
20
20
  }
21
+ export function _hasEmptyAgentEndContent(content) {
22
+ return content == null || (Array.isArray(content) && content.length === 0);
23
+ }
21
24
  /**
22
25
  * Cap on auto-resume attempts for sustained transient-provider errors.
23
26
  *
@@ -246,7 +249,7 @@ export async function handleAgentEnd(pi, event, ctx) {
246
249
  // that carry error context — e.g. errorMessage field or non-empty content
247
250
  // indicating a mid-stream failure. (#2695)
248
251
  const content = "content" in lastMsg ? lastMsg.content : undefined;
249
- const hasEmptyContent = Array.isArray(content) && content.length === 0;
252
+ const hasEmptyContent = _hasEmptyAgentEndContent(content);
250
253
  const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
251
254
  if (hasEmptyContent && !hasErrorMessage) {
252
255
  // Non-fatal: treat as a normal agent end so the loop can continue
@@ -59,6 +59,7 @@ const QUEUE_SAFE_TOOLS = new Set([
59
59
  * true / false — shell no-ops / test exit codes
60
60
  */
61
61
  const BASH_READ_ONLY_RE = /^\s*(cat|head|tail|less|more|wc|file|stat|du|df|which|type|echo|printf|ls|find|grep|rg|awk|sed\b(?!.*-i)|sort|uniq|diff|comm|tr|cut|tee\s+-a\s+\/dev\/null|git\s+(log|show|diff|status|branch|tag|remote|rev-parse|ls-files|blame|shortlog|describe|stash\s+list|config\s+--get|cat-file)|gh\s+(issue|pr|api|repo|release)\s+(view|list|diff|status|checks)|mkdir\s+-p\s+\.gsd|rtk\s|npm\s+run\s+(test|test:\w+|lint|lint:\w+|typecheck|type-check|type-check:\w+|check|verify|audit|outdated|format:check|ci|validate)\b|npm\s+(ls|list|info|view|show|outdated|audit|explain|doctor|ping|--version|-v)\b|npx\s|tsx\s|node\s+(--print|--version|-v\b)|python[23]?\s+(-c\s+'[^']*'|--version|-V\b|-m\s+(pip\s+show|pip\s+list|site))|pip[23]?\s+(show|list|freeze|check|index\s+versions)\b|jq\s|yq\s|curl\s+(-s\b|--silent\b)(?!\s+[^|>]*\s-[oO]\b)(?!\s+[^|>]*\s--output\b)[^|>]*$|openssl\s+(version|x509|s_client)|env\b|printenv\b|true\b|false\b)/;
62
+ const BASH_VERIFICATION_RE = /^\s*(npm\s+(run\s+(build|test|test:\w+|lint|lint:\w+|typecheck|type-check|verify|ci|validate)\b|test\b)|pnpm\s+(build|test|lint|typecheck|verify)\b|yarn\s+(build|test|lint|typecheck|verify)\b|vitest\b|jest\b|go\s+test\b)/;
62
63
  function createEmptyWriteGateState() {
63
64
  return {
64
65
  verifiedDepthMilestones: new Set(),
@@ -643,6 +644,9 @@ function blockReason(unitType, mode, what) {
643
644
  * and listed in the policy's allowedSubagents.
644
645
  * - "docs" → like "planning" but also allows writes to paths
645
646
  * matching `allowedPathGlobs` relative to basePath.
647
+ * - "verification"
648
+ * → allows Bash for project verification commands, but keeps
649
+ * writes restricted to .gsd/ and blocks subagent dispatch.
646
650
  *
647
651
  * `pathOrCommand` is the file path for write/edit-shaped tools and the
648
652
  * shell command for bash. Other tools ignore this argument.
@@ -674,7 +678,7 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
674
678
  // Unknown tool in read-only mode — block by default.
675
679
  return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
676
680
  }
677
- // planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
681
+ // planning / planning-dispatch / docs / verification modes share the same surface for safe tools, bash, and subagent.
678
682
  if (PLANNING_SAFE_TOOLS.has(tool))
679
683
  return { block: false };
680
684
  if (tool.startsWith("gsd_"))
@@ -720,6 +724,14 @@ export function shouldBlockPlanningUnit(toolName, pathOrCommand, basePath, unitT
720
724
  return { block: true, reason: blockReason(unitType, policy.mode, `subagent dispatch is not permitted in planning units`) };
721
725
  }
722
726
  if (tool === "bash") {
727
+ if (policy.mode === "verification") {
728
+ if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand))
729
+ return { block: false };
730
+ return {
731
+ block: true,
732
+ reason: blockReason(unitType, policy.mode, `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`),
733
+ };
734
+ }
723
735
  if (BASH_READ_ONLY_RE.test(pathOrCommand))
724
736
  return { block: false };
725
737
  return {
@@ -1,3 +1,4 @@
1
+ import { createRequire } from "node:module";
1
2
  import { computeProgressScore, formatProgressLine } from "../../progress-score.js";
2
3
  import { getGlobalGSDPreferencesPath, getProjectGSDPreferencesPath } from "../../preferences.js";
3
4
  import { ensurePreferencesFile, handlePrefs, handlePrefsMode, handlePrefsWizard, handleLanguage } from "../../commands-prefs-wizard.js";
@@ -197,7 +198,22 @@ export async function handleBrief(args, ctx, pi) {
197
198
  return;
198
199
  }
199
200
  const outputDir = getVisualBriefOutputDir();
200
- pi.sendUserMessage(buildVisualBriefPrompt(request, { outputDir }));
201
+ const version = resolveGsdVersion();
202
+ pi.sendUserMessage(buildVisualBriefPrompt(request, { outputDir, version }));
203
+ }
204
+ const briefRequire = createRequire(import.meta.url);
205
+ function resolveGsdVersion() {
206
+ const envVersion = process.env.GSD_VERSION?.trim();
207
+ if (envVersion)
208
+ return envVersion;
209
+ try {
210
+ const pkg = briefRequire("../../../../../../package.json");
211
+ const fromPkg = typeof pkg.version === "string" ? pkg.version.trim() : "";
212
+ return fromPkg || undefined;
213
+ }
214
+ catch {
215
+ return undefined;
216
+ }
201
217
  }
202
218
  export async function handleSetup(args, ctx, pi) {
203
219
  const { detectProjectState, hasGlobalSetup } = await import("../../detection.js");
@@ -391,7 +391,7 @@ export function getRecentUnitKeysForProjectRoot(projectRootRealpath, limit = 20)
391
391
  if (!isDbAvailable())
392
392
  return [];
393
393
  const db = _getAdapter();
394
- const rows = db.prepare(`SELECT ud.unit_id
394
+ const rows = db.prepare(`SELECT ud.unit_type, ud.unit_id
395
395
  FROM unit_dispatches ud
396
396
  INNER JOIN workers w ON w.worker_id = ud.worker_id
397
397
  WHERE w.project_root_realpath = :project_root_realpath
@@ -400,7 +400,7 @@ export function getRecentUnitKeysForProjectRoot(projectRootRealpath, limit = 20)
400
400
  ":project_root_realpath": projectRootRealpath,
401
401
  ":limit": limit,
402
402
  });
403
- return rows.reverse().map((r) => ({ key: r.unit_id }));
403
+ return rows.reverse().map((r) => ({ key: `${r.unit_type}/${r.unit_id}` }));
404
404
  }
405
405
  /**
406
406
  * Fetch dispatches for a milestone filtered by status. Useful for janitors