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
@@ -41,6 +41,7 @@ import {
41
41
  resolveExpectedArtifactPath,
42
42
  writeBlockerPlaceholder,
43
43
  diagnoseExpectedArtifact,
44
+ diagnoseWorktreeIntegrityFailure,
44
45
  } from "./auto-recovery.js";
45
46
  import { regenerateIfMissing } from "./workflow-projections.js";
46
47
  import { WorktreeStateProjection } from "./worktree-state-projection.js";
@@ -381,6 +382,23 @@ export function buildStepCompleteMessage(nextState: import("./types.js").GSDStat
381
382
  + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
382
383
  }
383
384
 
385
+ /**
386
+ * Decide whether step mode should stop at the step wizard after a unit finishes.
387
+ *
388
+ * @param currentUnitType The just-finished unit type, such as "execute-task" or
389
+ * "complete-milestone"; may be null/undefined when no current unit is known.
390
+ * @param phaseAfterUnit The freshly derived next phase, such as "executing" or
391
+ * "complete"; may be null/undefined if state derivation failed.
392
+ * @returns true to show the step wizard; false to keep the loop running so
393
+ * terminal milestone completion can reach the merge/finalization path.
394
+ */
395
+ export function shouldReturnStepWizardAfterUnit(
396
+ currentUnitType: string | null | undefined,
397
+ phaseAfterUnit: string | null | undefined,
398
+ ): boolean {
399
+ return currentUnitType !== "complete-milestone" && phaseAfterUnit !== "complete";
400
+ }
401
+
384
402
  export interface PreVerificationOpts {
385
403
  skipSettleDelay?: boolean;
386
404
  skipWorktreeSync?: boolean;
@@ -413,6 +431,11 @@ function artifactValidationKind(unitType: string): "project" | "requirements" |
413
431
  }
414
432
 
415
433
  function describeArtifactVerificationFailure(unitType: string, unitId: string, basePath: string): string {
434
+ const worktreeFailure = diagnoseWorktreeIntegrityFailure(basePath);
435
+ if (worktreeFailure) {
436
+ return `${worktreeFailure} Unit: ${unitType} ${unitId}.`;
437
+ }
438
+
416
439
  const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
417
440
  if (!artifactPath) {
418
441
  return `Artifact verification failed: ${unitType} "${unitId}" has no resolvable artifact path.`;
@@ -469,9 +492,17 @@ export async function autoCommitUnit(
469
492
  }
470
493
  }
471
494
 
495
+ /**
496
+ * Execute the turn-level git action (commit, snapshot, or status-only).
497
+ *
498
+ * @param opts.softFailure - Defaults to false. When true, retry git failures,
499
+ * warn, and continue without pausing auto-mode; use for best-effort deferred
500
+ * closeout work where a git failure should not block the run.
501
+ */
472
502
  async function runCloseoutGitAction(
473
503
  pctx: PostUnitContext,
474
504
  unit: NonNullable<AutoSession["currentUnit"]>,
505
+ opts?: { softFailure?: boolean },
475
506
  ): Promise<"continue" | "dispatched"> {
476
507
  const { s, ctx, pi, pauseAuto } = pctx;
477
508
  const prefs = loadEffectiveGSDPreferences()?.preferences;
@@ -506,13 +537,24 @@ async function runCloseoutGitAction(
506
537
  unitId: unit.id,
507
538
  });
508
539
  } else {
509
- const gitResult = runTurnGitAction({
540
+ const maxAttempts = opts?.softFailure ? 3 : 1;
541
+ let gitResult = runTurnGitAction({
510
542
  basePath: s.basePath,
511
543
  action: turnAction,
512
544
  unitType: unit.type,
513
545
  unitId: unit.id,
514
546
  taskContext,
515
547
  });
548
+ for (let attempt = 1; gitResult.status === "failed" && attempt < maxAttempts; attempt++) {
549
+ await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
550
+ gitResult = runTurnGitAction({
551
+ basePath: s.basePath,
552
+ action: turnAction,
553
+ unitType: unit.type,
554
+ unitId: unit.id,
555
+ taskContext,
556
+ });
557
+ }
516
558
 
517
559
  if (uokFlags.gitops) {
518
560
  writeTurnGitTransaction({
@@ -563,12 +605,15 @@ async function runCloseoutGitAction(
563
605
  }
564
606
 
565
607
  const failureMsg = `Git ${turnAction} failed: ${(gitResult.error ?? "unknown error").split("\n")[0]}`;
566
- ctx.ui.notify(failureMsg, "error");
608
+ ctx.ui.notify(failureMsg, opts?.softFailure ? "warning" : "error");
567
609
  debugLog("postUnit", {
568
- phase: "git-action-failed-blocking",
610
+ phase: opts?.softFailure ? "git-action-failed-soft" : "git-action-failed-blocking",
569
611
  action: turnAction,
570
612
  error: gitResult.error ?? "unknown error",
571
613
  });
614
+ if (opts?.softFailure) {
615
+ return "continue";
616
+ }
572
617
  await pauseAuto(ctx, pi);
573
618
  return "dispatched";
574
619
  }
@@ -586,7 +631,10 @@ async function runCloseoutGitAction(
586
631
  s.lastGitActionFailure = message;
587
632
  s.lastGitActionStatus = "failed";
588
633
  debugLog("postUnit", { phase: "git-action", error: message, action: turnAction });
589
- ctx.ui.notify(`Git ${turnAction} failed: ${message.split("\n")[0]}`, uokFlags.gitops ? "error" : "warning");
634
+ ctx.ui.notify(`Git ${turnAction} failed: ${message.split("\n")[0]}`, opts?.softFailure ? "warning" : "error");
635
+ if (opts?.softFailure) {
636
+ return "continue";
637
+ }
590
638
  if (uokFlags.gitops) {
591
639
  await pauseAuto(ctx, pi);
592
640
  return "dispatched";
@@ -1133,6 +1181,24 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
1133
1181
  "warning",
1134
1182
  );
1135
1183
  // Fall through to "continue" — do NOT enter the retry or db-unavailable paths.
1184
+ } else if (!triggerArtifactVerified && diagnoseWorktreeIntegrityFailure(s.basePath)) {
1185
+ const retryKey = `${s.currentUnit.type}:${s.currentUnit.id}`;
1186
+ const worktreeFailure = diagnoseWorktreeIntegrityFailure(s.basePath)!;
1187
+ s.pendingVerificationRetry = null;
1188
+ s.verificationRetryCount.delete(retryKey);
1189
+ s.verificationRetryFailureHashes.delete(retryKey);
1190
+ debugLog("postUnit", {
1191
+ phase: "worktree-integrity-failure",
1192
+ unitType: s.currentUnit.type,
1193
+ unitId: s.currentUnit.id,
1194
+ basePath: s.basePath,
1195
+ });
1196
+ ctx.ui.notify(
1197
+ `${worktreeFailure} Retry ${s.currentUnit.id} after repair.`,
1198
+ "error",
1199
+ );
1200
+ await pauseAuto(ctx, pi);
1201
+ return "dispatched";
1136
1202
  } else if (!triggerArtifactVerified && !isDbAvailable()) {
1137
1203
  debugLog("postUnit", { phase: "artifact-verify-skip-db-unavailable", unitType: s.currentUnit.type, unitId: s.currentUnit.id });
1138
1204
  const dbSkipDiag = diagnoseExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
@@ -1220,7 +1286,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1220
1286
 
1221
1287
  if (s.currentUnit) {
1222
1288
  if (shouldDeferCloseoutGitAction(s.currentUnit.type)) {
1223
- const gitActionResult = await runCloseoutGitAction(pctx, s.currentUnit);
1289
+ const gitActionResult = await runCloseoutGitAction(pctx, s.currentUnit, { softFailure: true });
1224
1290
  if (gitActionResult === "dispatched") {
1225
1291
  return "stopped";
1226
1292
  }
@@ -1668,14 +1734,18 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1668
1734
  // Without this notify(), /gsd in step mode finishes a unit and silently
1669
1735
  // exits the loop, leaving the user with no hint to /clear and /gsd again.
1670
1736
  if (s.stepMode) {
1737
+ let phaseAfterUnit: string | null = null;
1671
1738
  try {
1672
1739
  const nextState = await deriveState(s.canonicalProjectRoot);
1740
+ phaseAfterUnit = nextState.phase;
1673
1741
  ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
1674
1742
  } catch (e) {
1675
1743
  debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
1676
1744
  ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
1677
1745
  }
1678
- return "step-wizard";
1746
+ return shouldReturnStepWizardAfterUnit(s.currentUnit?.type, phaseAfterUnit)
1747
+ ? "step-wizard"
1748
+ : "continue";
1679
1749
  }
1680
1750
 
1681
1751
  return "continue";
@@ -46,6 +46,7 @@ import {
46
46
  import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.js";
47
47
  import { validateArtifact } from "./schemas/validate.js";
48
48
  import { getProjectResearchStatus } from "./project-research-policy.js";
49
+ import { isGsdWorktreePath } from "./worktree-root.js";
49
50
 
50
51
  // Re-export so existing consumers of auto-recovery.ts keep working.
51
52
  export { resolveExpectedArtifactPath, diagnoseExpectedArtifact };
@@ -56,6 +57,29 @@ export {
56
57
 
57
58
  // ─── Artifact Resolution & Verification ───────────────────────────────────────
58
59
 
60
+ export function diagnoseWorktreeIntegrityFailure(basePath: string): string | null {
61
+ if (!isGsdWorktreePath(basePath)) return null;
62
+ if (!existsSync(basePath)) {
63
+ return `Worktree integrity failure: ${basePath} does not exist. Repair or recreate the worktree before retrying.`;
64
+ }
65
+
66
+ const gitPath = join(basePath, ".git");
67
+ if (!existsSync(gitPath)) {
68
+ return `Worktree integrity failure: ${basePath} is not a valid git worktree (.git missing). Repair or recreate the worktree before retrying.`;
69
+ }
70
+
71
+ try {
72
+ execFileSync("git", ["rev-parse", "--git-dir"], {
73
+ cwd: basePath,
74
+ stdio: ["ignore", "pipe", "pipe"],
75
+ encoding: "utf-8",
76
+ });
77
+ return null;
78
+ } catch (err) {
79
+ 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.`;
80
+ }
81
+ }
82
+
59
83
  export type ArtifactRecoveryDbRefreshResult =
60
84
  | { ok: true }
61
85
  | { ok: false; fatal: boolean; message: string; reason: string };
@@ -752,6 +776,11 @@ export function verifyExpectedArtifact(
752
776
  return false;
753
777
  }
754
778
  if (!existsSync(absPath)) {
779
+ const worktreeFailure = diagnoseWorktreeIntegrityFailure(base);
780
+ if (worktreeFailure) {
781
+ logError("recovery", `${worktreeFailure} Unit: ${unitType} ${unitId}.`);
782
+ return false;
783
+ }
755
784
  logWarning("recovery", `verify-fail ${unitType} ${unitId}: existsSync false for ${absPath}`);
756
785
  return false;
757
786
  }
@@ -818,14 +818,18 @@ export async function bootstrapAutoSession(
818
818
  // worktree cleanup) was never run — the survivor branch must be merged.
819
819
  // Applies to both worktree and branch isolation modes.
820
820
  let hasSurvivorBranch = false;
821
+ let survivorMilestoneId = state.activeMilestone?.id ?? null;
822
+ if (!survivorMilestoneId && state.phase === "complete") {
823
+ survivorMilestoneId = findUnmergedCompletedMilestone(base, getIsolationMode(base));
824
+ }
821
825
  if (
822
- state.activeMilestone &&
826
+ survivorMilestoneId &&
823
827
  (state.phase === "pre-planning" || state.phase === "complete") &&
824
828
  getIsolationMode(base) !== "none" &&
825
829
  !detectWorktreeName(base) &&
826
830
  !base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)
827
831
  ) {
828
- const milestoneBranch = `milestone/${state.activeMilestone.id}`;
832
+ const milestoneBranch = `milestone/${survivorMilestoneId}`;
829
833
  const { nativeBranchExists } = await import("./native-git-bridge.js");
830
834
  hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
831
835
  if (hasSurvivorBranch) {
@@ -869,7 +873,7 @@ export async function bootstrapAutoSession(
869
873
  // Re-evaluate via the helper — the discuss branch above may have cleared
870
874
  // hasSurvivorBranch after a successful promotion.
871
875
  if (decideSurvivorAction(hasSurvivorBranch, state.phase) === "finalize") {
872
- const mid = state.activeMilestone!.id;
876
+ const mid = survivorMilestoneId!;
873
877
  // Commit 68ef58a3c made `_mergeBranchMode` throw on wrong-branch
874
878
  // instead of returning false silently. Wrap the call so the throw is
875
879
  // converted into an error notify + clean bootstrap abort, not an
@@ -282,6 +282,50 @@ function gitRemoteExists(basePath: string, remote: string): boolean {
282
282
  }
283
283
  }
284
284
 
285
+ function findRegularMergeChangedPaths(basePath: string, milestoneBranch: string, mainBranch: string): Set<string> {
286
+ const changedPaths = new Set<string>();
287
+ let mergeLog = "";
288
+ try {
289
+ mergeLog = execFileSync("git", ["rev-list", "--merges", "--parents", mainBranch], {
290
+ cwd: basePath,
291
+ stdio: ["ignore", "pipe", "pipe"],
292
+ encoding: "utf-8",
293
+ }).trim();
294
+ } catch (err) {
295
+ logWarning("worktree", `regular merge lookup failed: ${err instanceof Error ? err.message : String(err)}`);
296
+ return changedPaths;
297
+ }
298
+
299
+ for (const line of mergeLog.split("\n").filter(Boolean)) {
300
+ const [mergeCommit, firstParent, ...otherParents] = line.split(" ");
301
+ if (!mergeCommit || !firstParent || otherParents.length === 0) continue;
302
+ const mergedMilestone = otherParents.some((parent) => {
303
+ try {
304
+ return nativeIsAncestor(basePath, milestoneBranch, parent);
305
+ } catch {
306
+ return false;
307
+ }
308
+ });
309
+ if (!mergedMilestone) continue;
310
+
311
+ try {
312
+ const output = execFileSync("git", ["diff", "--name-only", firstParent, mergeCommit], {
313
+ cwd: basePath,
314
+ stdio: ["ignore", "pipe", "pipe"],
315
+ encoding: "utf-8",
316
+ }).trim();
317
+ for (const path of output.split("\n").filter(Boolean)) {
318
+ if (!path.startsWith(".gsd/")) changedPaths.add(path);
319
+ }
320
+ } catch (err) {
321
+ logWarning("worktree", `regular merge diff lookup failed: ${err instanceof Error ? err.message : String(err)}`);
322
+ }
323
+ return changedPaths;
324
+ }
325
+
326
+ return changedPaths;
327
+ }
328
+
285
329
  function clearProjectRootStateFiles(basePath: string, milestoneId: string): void {
286
330
  const gsdDir = gsdRoot(basePath);
287
331
  // Phase C pt 2: auto.lock removed from this list — the file is gone
@@ -1739,6 +1783,66 @@ export function mergeMilestoneToMain(
1739
1783
  }
1740
1784
  }
1741
1785
 
1786
+ // Already regular-merged milestones can skip the squash path and proceed to cleanup (#5831).
1787
+ if (nativeIsAncestor(originalBasePath_, milestoneBranch, mainBranch)) {
1788
+ const codeChanges = nativeDiffNumstat(
1789
+ originalBasePath_,
1790
+ mainBranch,
1791
+ milestoneBranch,
1792
+ ).filter((entry) => !entry.path.startsWith(".gsd/"));
1793
+ if (codeChanges.length > 0) {
1794
+ const regularMergeChangedPaths = findRegularMergeChangedPaths(
1795
+ originalBasePath_,
1796
+ milestoneBranch,
1797
+ mainBranch,
1798
+ );
1799
+ const unanchoredCodeChanges = codeChanges.filter((entry) =>
1800
+ regularMergeChangedPaths.has(entry.path)
1801
+ );
1802
+ if (unanchoredCodeChanges.length > 0) {
1803
+ process.chdir(previousCwd);
1804
+ throw new GSDError(
1805
+ GSD_GIT_ERROR,
1806
+ `Milestone branch "${milestoneBranch}" is reachable from "${mainBranch}" ` +
1807
+ `but has ${unanchoredCodeChanges.length} milestone-touched code file(s) not on current "${mainBranch}". ` +
1808
+ `Aborting worktree teardown to prevent data loss.`,
1809
+ );
1810
+ }
1811
+ }
1812
+ debugLog("mergeMilestoneToMain", {
1813
+ action: "skip-squash-already-merged",
1814
+ milestoneId,
1815
+ milestoneBranch,
1816
+ mainBranch,
1817
+ });
1818
+ try {
1819
+ clearProjectRootStateFiles(originalBasePath_, milestoneId);
1820
+ } catch (err) {
1821
+ logWarning("worktree", `clearProjectRootStateFiles failed during already-merged cleanup: ${err instanceof Error ? err.message : String(err)}`);
1822
+ }
1823
+ try {
1824
+ removeWorktree(originalBasePath_, milestoneId, {
1825
+ branch: milestoneBranch,
1826
+ deleteBranch: false,
1827
+ });
1828
+ } catch (err) {
1829
+ logWarning("worktree", `worktree removal failed: ${err instanceof Error ? err.message : String(err)}`);
1830
+ }
1831
+ try {
1832
+ nativeBranchDelete(originalBasePath_, milestoneBranch);
1833
+ } catch (err) {
1834
+ logWarning("worktree", `git branch-delete failed: ${err instanceof Error ? err.message : String(err)}`);
1835
+ }
1836
+ setActiveWorkspace(null);
1837
+ nudgeGitBranchCache(previousCwd);
1838
+ try {
1839
+ process.chdir(originalBasePath_);
1840
+ } catch (err) {
1841
+ logWarning("worktree", `chdir to project root after already-merged cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
1842
+ }
1843
+ return { commitMessage, pushed: false, prCreated: false, codeFilesChanged: true };
1844
+ }
1845
+
1742
1846
  // 7. Shelter queued milestone directories before the squash merge (#2505).
1743
1847
  // The milestone branch may contain copies of queued milestone dirs (via
1744
1848
  // copyPlanningArtifacts), so `git merge --squash` rejects when those same
@@ -48,6 +48,11 @@ const MAX_NETWORK_RETRIES = 2;
48
48
  function isObjectRecord(value: unknown): value is Record<string, unknown> {
49
49
  return !!value && typeof value === "object";
50
50
  }
51
+
52
+ export function _hasEmptyAgentEndContent(content: unknown): boolean {
53
+ return content == null || (Array.isArray(content) && content.length === 0);
54
+ }
55
+
51
56
  /**
52
57
  * Cap on auto-resume attempts for sustained transient-provider errors.
53
58
  *
@@ -310,7 +315,7 @@ export async function handleAgentEnd(
310
315
  // that carry error context — e.g. errorMessage field or non-empty content
311
316
  // indicating a mid-stream failure. (#2695)
312
317
  const content = "content" in lastMsg ? lastMsg.content : undefined;
313
- const hasEmptyContent = Array.isArray(content) && content.length === 0;
318
+ const hasEmptyContent = _hasEmptyAgentEndContent(content);
314
319
  const hasErrorMessage = "errorMessage" in lastMsg && !!lastMsg.errorMessage;
315
320
 
316
321
  if (hasEmptyContent && !hasErrorMessage) {
@@ -65,6 +65,7 @@ const QUEUE_SAFE_TOOLS = new Set([
65
65
  * true / false — shell no-ops / test exit codes
66
66
  */
67
67
  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)/;
68
+ 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)/;
68
69
 
69
70
  interface InMemoryWriteGateState {
70
71
  verifiedDepthMilestones: Set<string>;
@@ -767,6 +768,9 @@ function blockReason(unitType: string, mode: string, what: string): string {
767
768
  * and listed in the policy's allowedSubagents.
768
769
  * - "docs" → like "planning" but also allows writes to paths
769
770
  * matching `allowedPathGlobs` relative to basePath.
771
+ * - "verification"
772
+ * → allows Bash for project verification commands, but keeps
773
+ * writes restricted to .gsd/ and blocks subagent dispatch.
770
774
  *
771
775
  * `pathOrCommand` is the file path for write/edit-shaped tools and the
772
776
  * shell command for bash. Other tools ignore this argument.
@@ -804,7 +808,7 @@ export function shouldBlockPlanningUnit(
804
808
  return { block: true, reason: blockReason(unitType, policy.mode, `tool "${tool}" is not on the read-only allowlist`) };
805
809
  }
806
810
 
807
- // planning / planning-dispatch / docs modes share the same surface for safe tools, bash, and subagent.
811
+ // planning / planning-dispatch / docs / verification modes share the same surface for safe tools, bash, and subagent.
808
812
  if (PLANNING_SAFE_TOOLS.has(tool)) return { block: false };
809
813
  if (tool.startsWith("gsd_")) return { block: false };
810
814
 
@@ -862,6 +866,17 @@ export function shouldBlockPlanningUnit(
862
866
  }
863
867
 
864
868
  if (tool === "bash") {
869
+ if (policy.mode === "verification") {
870
+ if (BASH_VERIFICATION_RE.test(pathOrCommand) || BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
871
+ return {
872
+ block: true,
873
+ reason: blockReason(
874
+ unitType,
875
+ policy.mode,
876
+ `bash is restricted to build/test verification commands (npm run build, npm test, etc.); cannot run "${pathOrCommand.slice(0, 80)}${pathOrCommand.length > 80 ? "…" : ""}"`,
877
+ ),
878
+ };
879
+ }
865
880
  if (BASH_READ_ONLY_RE.test(pathOrCommand)) return { block: false };
866
881
  return {
867
882
  block: true,
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@gsd/pi-coding-agent";
2
2
  import type { Model } from "@gsd/pi-ai";
3
3
  import type { GSDState } from "../../types.js";
4
+ import { createRequire } from "node:module";
4
5
 
5
6
  import { computeProgressScore, formatProgressLine } from "../../progress-score.js";
6
7
  import { loadEffectiveGSDPreferences, getGlobalGSDPreferencesPath, getProjectGSDPreferencesPath } from "../../preferences.js";
@@ -220,7 +221,22 @@ export async function handleBrief(args: string, ctx: ExtensionCommandContext, pi
220
221
  }
221
222
 
222
223
  const outputDir = getVisualBriefOutputDir();
223
- pi.sendUserMessage(buildVisualBriefPrompt(request, { outputDir }));
224
+ const version = resolveGsdVersion();
225
+ pi.sendUserMessage(buildVisualBriefPrompt(request, { outputDir, version }));
226
+ }
227
+
228
+ const briefRequire = createRequire(import.meta.url);
229
+
230
+ function resolveGsdVersion(): string | undefined {
231
+ const envVersion = process.env.GSD_VERSION?.trim();
232
+ if (envVersion) return envVersion;
233
+ try {
234
+ const pkg = briefRequire("../../../../../../package.json") as { version?: unknown };
235
+ const fromPkg = typeof pkg.version === "string" ? pkg.version.trim() : "";
236
+ return fromPkg || undefined;
237
+ } catch {
238
+ return undefined;
239
+ }
224
240
  }
225
241
 
226
242
  export async function handleSetup(args: string, ctx: ExtensionCommandContext, pi?: ExtensionAPI): Promise<void> {
@@ -524,7 +524,7 @@ export function getRecentUnitKeysForProjectRoot(
524
524
  if (!isDbAvailable()) return [];
525
525
  const db = _getAdapter()!;
526
526
  const rows = db.prepare(
527
- `SELECT ud.unit_id
527
+ `SELECT ud.unit_type, ud.unit_id
528
528
  FROM unit_dispatches ud
529
529
  INNER JOIN workers w ON w.worker_id = ud.worker_id
530
530
  WHERE w.project_root_realpath = :project_root_realpath
@@ -533,8 +533,8 @@ export function getRecentUnitKeysForProjectRoot(
533
533
  ).all({
534
534
  ":project_root_realpath": projectRootRealpath,
535
535
  ":limit": limit,
536
- }) as Array<{ unit_id: string }>;
537
- return rows.reverse().map((r) => ({ key: r.unit_id }));
536
+ }) as Array<{ unit_type: string; unit_id: string }>;
537
+ return rows.reverse().map((r) => ({ key: `${r.unit_type}/${r.unit_id}` }));
538
538
  }
539
539
 
540
540
  /**