gsd-pi 2.73.1-dev.6ddfa43 → 2.73.1-dev.9a4cd44

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 (126) hide show
  1. package/dist/cli-web-branch.d.ts +4 -3
  2. package/dist/cli-web-branch.js +10 -7
  3. package/dist/cli.js +99 -206
  4. package/dist/logo.d.ts +1 -1
  5. package/dist/logo.js +1 -1
  6. package/dist/onboarding.js +59 -53
  7. package/dist/resource-loader.js +2 -2
  8. package/dist/resources/extensions/gsd/auto/phases.js +15 -9
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +41 -1
  11. package/dist/resources/extensions/gsd/auto-start.js +3 -0
  12. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
  13. package/dist/resources/extensions/gsd/auto-verification.js +88 -3
  14. package/dist/resources/extensions/gsd/auto.js +29 -8
  15. package/dist/resources/extensions/gsd/commands-handlers.js +8 -2
  16. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  17. package/dist/resources/extensions/gsd/notification-widget.js +2 -2
  18. package/dist/resources/extensions/gsd/state.js +61 -14
  19. package/dist/update-check.d.ts +1 -0
  20. package/dist/update-check.js +13 -5
  21. package/dist/update-cmd.js +4 -3
  22. package/dist/web/standalone/.next/BUILD_ID +1 -1
  23. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  24. package/dist/web/standalone/.next/build-manifest.json +2 -2
  25. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  26. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.html +1 -1
  43. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  50. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  51. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  52. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  53. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  54. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  55. package/package.json +1 -2
  56. package/packages/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  57. package/packages/pi-ai/dist/utils/overflow.js +12 -0
  58. package/packages/pi-ai/dist/utils/overflow.js.map +1 -1
  59. package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts +2 -0
  60. package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts.map +1 -0
  61. package/packages/pi-ai/dist/utils/tests/overflow.test.js +50 -0
  62. package/packages/pi-ai/dist/utils/tests/overflow.test.js.map +1 -0
  63. package/packages/pi-ai/src/utils/overflow.ts +14 -1
  64. package/packages/pi-ai/src/utils/tests/overflow.test.ts +58 -0
  65. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +138 -0
  66. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/compaction/utils.js +5 -5
  68. package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts +2 -0
  70. package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts.map +1 -0
  71. package/packages/pi-coding-agent/dist/core/compaction-utils.test.js +45 -0
  72. package/packages/pi-coding-agent/dist/core/compaction-utils.test.js.map +1 -0
  73. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
  81. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  82. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +21 -4
  83. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +11 -3
  86. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  87. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +157 -0
  88. package/packages/pi-coding-agent/src/core/compaction/utils.ts +5 -5
  89. package/packages/pi-coding-agent/src/core/compaction-utils.test.ts +50 -0
  90. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
  91. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
  92. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -4
  93. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +11 -3
  94. package/packages/pi-tui/dist/__tests__/tui.test.js +60 -1
  95. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
  96. package/packages/pi-tui/dist/tui.d.ts +8 -0
  97. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  98. package/packages/pi-tui/dist/tui.js +32 -3
  99. package/packages/pi-tui/dist/tui.js.map +1 -1
  100. package/packages/pi-tui/src/__tests__/tui.test.ts +76 -1
  101. package/packages/pi-tui/src/tui.ts +31 -3
  102. package/src/resources/extensions/gsd/auto/phases.ts +22 -9
  103. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
  104. package/src/resources/extensions/gsd/auto-post-unit.ts +47 -1
  105. package/src/resources/extensions/gsd/auto-start.ts +3 -0
  106. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
  107. package/src/resources/extensions/gsd/auto-verification.ts +98 -3
  108. package/src/resources/extensions/gsd/auto.ts +31 -14
  109. package/src/resources/extensions/gsd/commands-handlers.ts +8 -2
  110. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  111. package/src/resources/extensions/gsd/notification-widget.ts +2 -2
  112. package/src/resources/extensions/gsd/state.ts +71 -15
  113. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
  114. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
  115. package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
  116. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
  117. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
  118. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
  119. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
  120. package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
  121. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  122. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
  123. package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
  124. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
  125. /package/dist/web/standalone/.next/static/{r6AvNu-aMwn4nwqjHqAfw → ASJ2RGD7E1iiUYzA0xT2i}/_buildManifest.js +0 -0
  126. /package/dist/web/standalone/.next/static/{r6AvNu-aMwn4nwqjHqAfw → ASJ2RGD7E1iiUYzA0xT2i}/_ssgManifest.js +0 -0
@@ -104,6 +104,7 @@ import {
104
104
  updateSliceProgressCache,
105
105
  unitVerb,
106
106
  hideFooter,
107
+ describeNextUnit,
107
108
  } from "./auto-dashboard.js";
108
109
  import { existsSync, unlinkSync } from "node:fs";
109
110
  import { join } from "node:path";
@@ -233,6 +234,18 @@ export function detectRogueFileWrites(
233
234
  return rogues;
234
235
  }
235
236
 
237
+ export const STEP_COMPLETE_FALLBACK_MESSAGE =
238
+ "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).";
239
+
240
+ export function buildStepCompleteMessage(nextState: import("./types.js").GSDState): string {
241
+ if (nextState.phase === "complete") {
242
+ return "Step complete — milestone finished. Run /gsd status to review, or start the next milestone.";
243
+ }
244
+ const next = describeNextUnit(nextState);
245
+ return `Step complete. Next: ${next.label}\n`
246
+ + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
247
+ }
248
+
236
249
  export interface PreVerificationOpts {
237
250
  skipSettleDelay?: boolean;
238
251
  skipWorktreeSync?: boolean;
@@ -619,6 +632,30 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
619
632
  s.verificationRetryCount.set(retryKey, attempt);
620
633
 
621
634
  if (attempt > MAX_VERIFICATION_RETRIES) {
635
+ // #4175: For complete-milestone, a blocker placeholder is harmful —
636
+ // the stub SUMMARY has no recovery value (milestone is terminal),
637
+ // it does not update DB status (so deriveState never advances),
638
+ // and it fools stopAuto's presence check into merging a milestone
639
+ // that was never legitimately completed. Pause auto-mode with a
640
+ // clear single failure signal and preserve the worktree branch.
641
+ if (s.currentUnit.type === "complete-milestone") {
642
+ debugLog("postUnit", {
643
+ phase: "artifact-verify-pause-complete-milestone",
644
+ unitType: s.currentUnit.type,
645
+ unitId: s.currentUnit.id,
646
+ attempt,
647
+ maxRetries: MAX_VERIFICATION_RETRIES,
648
+ });
649
+ s.verificationRetryCount.delete(retryKey);
650
+ s.pendingVerificationRetry = null;
651
+ ctx.ui.notify(
652
+ `Milestone ${s.currentUnit.id} verification failed after ${MAX_VERIFICATION_RETRIES} retries — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`,
653
+ "error",
654
+ );
655
+ await pauseAuto(ctx, pi);
656
+ return "dispatched";
657
+ }
658
+
622
659
  // Retries exhausted — write a blocker placeholder so the pipeline
623
660
  // can advance past this stuck unit (#2653).
624
661
  debugLog("postUnit", {
@@ -1025,8 +1062,17 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
1025
1062
  }
1026
1063
  }
1027
1064
 
1028
- // Step mode → show wizard instead of dispatch
1065
+ // Step mode → show wizard instead of dispatch.
1066
+ // Without this notify(), /gsd in step mode finishes a unit and silently
1067
+ // exits the loop, leaving the user with no hint to /clear and /gsd again.
1029
1068
  if (s.stepMode) {
1069
+ try {
1070
+ const nextState = await deriveState(s.basePath);
1071
+ ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
1072
+ } catch (e) {
1073
+ debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
1074
+ ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
1075
+ }
1030
1076
  return "step-wizard";
1031
1077
  }
1032
1078
 
@@ -806,6 +806,9 @@ export async function bootstrapAutoSession(
806
806
 
807
807
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
808
808
  ctx.ui.setFooter(hideFooter);
809
+ // Hide gsd-health during AUTO — gsd-progress is the single source of truth
810
+ // for last-commit / cost / health signal while auto is running.
811
+ ctx.ui.setWidget("gsd-health", undefined);
809
812
  const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
810
813
  const pendingCount = (state.registry ?? []).filter(
811
814
  (m) => m.status !== "complete" && m.status !== "parked",
@@ -230,6 +230,23 @@ export async function recoverTimedOutUnit(
230
230
  return "recovered";
231
231
  }
232
232
 
233
+ // #4175: For complete-milestone, never write a blocker placeholder — a stub
234
+ // SUMMARY has no recovery value (milestone is terminal), it does not update
235
+ // DB status, and downstream merge paths can treat the stub as a legitimate
236
+ // completion signal. Pause instead so the worktree branch is preserved.
237
+ if (unitType === "complete-milestone") {
238
+ writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
239
+ phase: "paused",
240
+ recoveryAttempts: recoveryAttempts + 1,
241
+ lastRecoveryReason: reason,
242
+ });
243
+ ctx.ui.notify(
244
+ `Milestone ${unitId} ${reason}-recovery exhausted ${maxRecoveryAttempts} attempt(s) — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`,
245
+ "error",
246
+ );
247
+ return "paused";
248
+ }
249
+
233
250
  // Retries exhausted — write a blocker placeholder and advance the pipeline
234
251
  // instead of silently stalling.
235
252
  const placeholder = writeBlockerPlaceholder(
@@ -12,10 +12,15 @@
12
12
 
13
13
  import type { ExtensionContext, ExtensionAPI } from "@gsd/pi-coding-agent";
14
14
  import { mkdirSync, writeFileSync } from "node:fs";
15
- import { resolveSliceFile, resolveSlicePath } from "./paths.js";
15
+ import { resolveSliceFile, resolveSlicePath, resolveMilestoneFile } from "./paths.js";
16
16
  import { parseUnitId } from "./unit-id.js";
17
- import { isDbAvailable, getTask, getSliceTasks, type TaskRow } from "./gsd-db.js";
17
+ import { isDbAvailable, getTask, getSliceTasks, getMilestoneSlices, type TaskRow } from "./gsd-db.js";
18
18
  import { loadEffectiveGSDPreferences } from "./preferences.js";
19
+ import { extractVerdict } from "./verdict-parser.js";
20
+ import { isClosedStatus } from "./status-guards.js";
21
+ import { loadFile } from "./files.js";
22
+ import { parseRoadmap } from "./parsers-legacy.js";
23
+ import { isMilestoneComplete } from "./state.js";
19
24
  import {
20
25
  runVerificationGate,
21
26
  formatFailureContext,
@@ -43,6 +48,88 @@ function isInfraVerificationFailure(stderr: string): boolean {
43
48
  );
44
49
  }
45
50
 
51
+ /**
52
+ * Post-unit guard for `validate-milestone` units (#4094).
53
+ *
54
+ * When validate-milestone writes verdict=needs-remediation, the agent is
55
+ * expected to also call gsd_reassess_roadmap in the same turn to add
56
+ * remediation slices. If they don't, the state machine re-derives
57
+ * `phase: validating-milestone` indefinitely (all slices still complete +
58
+ * verdict still needs-remediation), wasting ~3 dispatches before the stuck
59
+ * detector fires.
60
+ *
61
+ * This guard fires immediately on the first occurrence: if VALIDATION.md
62
+ * verdict is needs-remediation and no incomplete slices exist for the
63
+ * milestone, pause the auto-loop with a clear blocker.
64
+ */
65
+ async function runValidateMilestonePostCheck(
66
+ vctx: VerificationContext,
67
+ pauseAuto: (ctx?: ExtensionContext, pi?: ExtensionAPI) => Promise<void>,
68
+ ): Promise<VerificationResult> {
69
+ const { s, ctx, pi } = vctx;
70
+ if (!s.currentUnit) return "continue";
71
+
72
+ const { milestone: mid } = parseUnitId(s.currentUnit.id);
73
+ if (!mid) return "continue";
74
+
75
+ const validationFile = resolveMilestoneFile(s.basePath, mid, "VALIDATION");
76
+ if (!validationFile) return "continue";
77
+
78
+ const validationContent = await loadFile(validationFile);
79
+ if (!validationContent) return "continue";
80
+
81
+ const verdict = extractVerdict(validationContent);
82
+ if (verdict !== "needs-remediation") return "continue";
83
+
84
+ const incompleteSliceCount = await countIncompleteSlices(s.basePath, mid);
85
+
86
+ // If any non-closed slices exist, the agent successfully queued remediation
87
+ // work — proceed normally. The state machine will execute those slices and
88
+ // re-validate per the #3596/#3670 fix.
89
+ if (incompleteSliceCount > 0) return "continue";
90
+
91
+ ctx.ui.notify(
92
+ `Milestone ${mid} validation returned verdict=needs-remediation but no remediation slices were added. Pausing for human review.`,
93
+ "error",
94
+ );
95
+ process.stderr.write(
96
+ `validate-milestone: pausing — verdict=needs-remediation with no incomplete slices for ${mid}. ` +
97
+ `The agent must call gsd_reassess_roadmap to add remediation slices before re-validation.\n`,
98
+ );
99
+ await pauseAuto(ctx, pi);
100
+ return "pause";
101
+ }
102
+
103
+ /**
104
+ * Count slices for a milestone that are not in a closed status.
105
+ * DB-backed projects are authoritative (#4094 peer review); falls back to
106
+ * roadmap parsing only when the DB is unavailable.
107
+ */
108
+ async function countIncompleteSlices(basePath: string, milestoneId: string): Promise<number> {
109
+ if (isDbAvailable()) {
110
+ const slices = getMilestoneSlices(milestoneId);
111
+ if (slices.length === 0) {
112
+ // No DB rows — treat as "unknown", do not pause.
113
+ return 1;
114
+ }
115
+ return slices.filter((slice) => !isClosedStatus(slice.status)).length;
116
+ }
117
+
118
+ // Filesystem fallback: parse the roadmap markdown.
119
+ try {
120
+ const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
121
+ if (!roadmapFile) return 1;
122
+ const roadmapContent = await loadFile(roadmapFile);
123
+ if (!roadmapContent) return 1;
124
+ const roadmap = parseRoadmap(roadmapContent);
125
+ if (roadmap.slices.length === 0) return 1;
126
+ return isMilestoneComplete(roadmap) ? 0 : 1;
127
+ } catch {
128
+ // Parsing failures should not cause false-positive pauses.
129
+ return 1;
130
+ }
131
+ }
132
+
46
133
  /**
47
134
  * Run the verification gate for the current execute-task unit.
48
135
  * Returns:
@@ -56,7 +143,15 @@ export async function runPostUnitVerification(
56
143
  ): Promise<VerificationResult> {
57
144
  const { s, ctx, pi } = vctx;
58
145
 
59
- if (!s.currentUnit || s.currentUnit.type !== "execute-task") {
146
+ if (!s.currentUnit) {
147
+ return "continue";
148
+ }
149
+
150
+ if (s.currentUnit.type === "validate-milestone") {
151
+ return await runValidateMilestonePostCheck(vctx, pauseAuto);
152
+ }
153
+
154
+ if (s.currentUnit.type !== "execute-task") {
60
155
  return "continue";
61
156
  }
62
157
 
@@ -187,7 +187,7 @@ import {
187
187
  deregisterSigtermHandler as _deregisterSigtermHandler,
188
188
  detectWorkingTreeActivity,
189
189
  } from "./auto-supervisor.js";
190
- import { isDbAvailable } from "./gsd-db.js";
190
+ import { isDbAvailable, getMilestone } from "./gsd-db.js";
191
191
  import { countPendingCaptures } from "./captures.js";
192
192
  import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
193
193
 
@@ -199,6 +199,7 @@ import {
199
199
  postUnitPostVerification,
200
200
  } from "./auto-post-unit.js";
201
201
  import { bootstrapAutoSession, openProjectDbIfPresent, type BootstrapDeps } from "./auto-start.js";
202
+ import { initHealthWidget } from "./health-widget.js";
202
203
  import { autoLoop, resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSessionSwitchInFlight, type LoopDeps, type ErrorContext } from "./auto-loop.js";
203
204
  // Slice-level parallelism (#2340)
204
205
  import { getEligibleSlices } from "./slice-parallel-eligibility.js";
@@ -650,6 +651,7 @@ function handleLostSessionLock(
650
651
  ctx?.ui.setStatus("gsd-auto", undefined);
651
652
  ctx?.ui.setWidget("gsd-progress", undefined);
652
653
  ctx?.ui.setFooter(undefined);
654
+ if (ctx) initHealthWidget(ctx);
653
655
  }
654
656
 
655
657
  /**
@@ -684,6 +686,7 @@ function cleanupAfterLoopExit(ctx: ExtensionContext): void {
684
686
  ctx.ui.setStatus("gsd-auto", undefined);
685
687
  ctx.ui.setWidget("gsd-progress", undefined);
686
688
  ctx.ui.setFooter(undefined);
689
+ initHealthWidget(ctx);
687
690
  }
688
691
 
689
692
  // Restore CWD out of worktree back to original project root
@@ -758,24 +761,36 @@ export async function stopAuto(
758
761
  : { notify: () => {} };
759
762
  const resolver = buildResolver();
760
763
 
761
- // Check if the milestone is complete SUMMARY file is the authoritative signal.
764
+ // Check if the milestone is complete. DB status is the authoritative
765
+ // signal — only a successful gsd_complete_milestone call flips it to
766
+ // "complete" (tools/complete-milestone.ts). SUMMARY file presence is
767
+ // NOT sufficient: a blocker placeholder stub or a partial write can
768
+ // leave a file behind without the milestone actually being done,
769
+ // which previously caused stopAuto to merge a failed milestone and
770
+ // emit a misleading metadata-only merge warning (#4175).
771
+ // DB-unavailable projects fall back to SUMMARY-file presence.
762
772
  let milestoneComplete = false;
763
773
  try {
764
- const summaryPath = resolveMilestoneFile(
765
- s.originalBasePath || s.basePath,
766
- s.currentMilestoneId,
767
- "SUMMARY",
768
- );
769
- if (!summaryPath) {
770
- // Also check in the worktree path (SUMMARY may not be synced yet)
771
- const wtSummaryPath = resolveMilestoneFile(
772
- s.basePath,
774
+ if (isDbAvailable()) {
775
+ const dbRow = getMilestone(s.currentMilestoneId);
776
+ milestoneComplete = dbRow?.status === "complete";
777
+ } else {
778
+ const summaryPath = resolveMilestoneFile(
779
+ s.originalBasePath || s.basePath,
773
780
  s.currentMilestoneId,
774
781
  "SUMMARY",
775
782
  );
776
- milestoneComplete = wtSummaryPath !== null;
777
- } else {
778
- milestoneComplete = true;
783
+ if (!summaryPath) {
784
+ // Also check in the worktree path (SUMMARY may not be synced yet)
785
+ const wtSummaryPath = resolveMilestoneFile(
786
+ s.basePath,
787
+ s.currentMilestoneId,
788
+ "SUMMARY",
789
+ );
790
+ milestoneComplete = wtSummaryPath !== null;
791
+ } else {
792
+ milestoneComplete = true;
793
+ }
779
794
  }
780
795
  } catch (err) {
781
796
  // Non-fatal — fall through to preserveBranch path
@@ -943,6 +958,7 @@ export async function stopAuto(
943
958
  ctx?.ui.setStatus("gsd-auto", undefined);
944
959
  ctx?.ui.setWidget("gsd-progress", undefined);
945
960
  ctx?.ui.setFooter(undefined);
961
+ if (ctx) initHealthWidget(ctx);
946
962
  restoreProjectRootEnv();
947
963
  restoreMilestoneLockEnv();
948
964
 
@@ -1044,6 +1060,7 @@ export async function pauseAuto(
1044
1060
  ctx?.ui.setStatus("gsd-auto", "paused");
1045
1061
  ctx?.ui.setWidget("gsd-progress", undefined);
1046
1062
  ctx?.ui.setFooter(undefined);
1063
+ if (ctx) initHealthWidget(ctx);
1047
1064
  const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
1048
1065
  ctx?.ui.notify(
1049
1066
  `${s.stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`,
@@ -28,6 +28,11 @@ import { loadPrompt } from "./prompt-loader.js";
28
28
  const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/gsd-pi/latest";
29
29
  const UPDATE_FETCH_TIMEOUT_MS = 5000;
30
30
 
31
+ function resolveInstallCommand(pkg: string): string {
32
+ if ('bun' in process.versions) return `bun add -g ${pkg}`;
33
+ return `npm install -g ${pkg}`;
34
+ }
35
+
31
36
  async function fetchLatestVersionForCommand(): Promise<string | null> {
32
37
  const controller = new AbortController();
33
38
  const timeout = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
@@ -431,8 +436,9 @@ export async function handleUpdate(ctx: ExtensionCommandContext): Promise<void>
431
436
 
432
437
  ctx.ui.notify(`Updating: v${current} → v${latest}...`, "info");
433
438
 
439
+ const installCmd = resolveInstallCommand(`${NPM_PACKAGE}@latest`);
434
440
  try {
435
- execSync(`npm install -g ${NPM_PACKAGE}@latest`, {
441
+ execSync(installCmd, {
436
442
  stdio: ["ignore", "pipe", "ignore"],
437
443
  });
438
444
  ctx.ui.notify(
@@ -441,7 +447,7 @@ export async function handleUpdate(ctx: ExtensionCommandContext): Promise<void>
441
447
  );
442
448
  } catch {
443
449
  ctx.ui.notify(
444
- `Update failed. Try manually: npm install -g ${NPM_PACKAGE}@latest`,
450
+ `Update failed. Try manually: ${installCmd}`,
445
451
  "error",
446
452
  );
447
453
  }
@@ -157,7 +157,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
157
157
 
158
158
  - `phases`: fine-grained control over which phases run. Usually set by `token_profile`, but can be overridden. Keys:
159
159
  - `skip_research`: boolean — skip milestone-level research. Default: `false`.
160
- - `reassess_after_slice`: boolean — run roadmap reassessment after each completed slice. Default: `false`.
160
+ - `reassess_after_slice`: boolean — run roadmap reassessment after each completed slice. Default: `true`.
161
161
  - `skip_reassess`: boolean — force-disable roadmap reassessment even if `reassess_after_slice` is enabled. Default: `false`.
162
162
  - `skip_slice_research`: boolean — skip per-slice research. Default: `false`.
163
163
 
@@ -1,6 +1,6 @@
1
1
  // GSD Extension — Notification Widget
2
2
  // Always-on ambient widget rendered belowEditor showing unread count and
3
- // the most recent notification message. Refreshes every 5 seconds.
3
+ // the most recent notification message. Refreshes every 30 seconds.
4
4
  // Widget key: "gsd-notifications", placement: "belowEditor"
5
5
 
6
6
  import type { ExtensionContext } from "@gsd/pi-coding-agent";
@@ -19,7 +19,7 @@ export function buildNotificationWidgetLines(): string[] {
19
19
 
20
20
  // ─── Widget init ────────────────────────────────────────────────────────
21
21
 
22
- const REFRESH_INTERVAL_MS = 5_000;
22
+ const REFRESH_INTERVAL_MS = 30_000;
23
23
 
24
24
  /**
25
25
  * Initialize the always-on notification widget (belowEditor).
@@ -386,6 +386,10 @@ function buildCompletenessSet(basePath: string, milestones: MilestoneRow[]) {
386
386
  const completeMilestoneIds = new Set<string>();
387
387
  const parkedMilestoneIds = new Set<string>();
388
388
 
389
+ // DB-authoritative: a milestone is only "complete" when its DB row says so.
390
+ // SUMMARY-file presence is NOT a completion signal here — an orphan SUMMARY
391
+ // (crashed complete-milestone turn, partial merge, manual edit) must not
392
+ // flip derived state to complete and cascade into a false auto-merge (#4179).
389
393
  for (const m of milestones) {
390
394
  const parkedFile = resolveMilestoneFile(basePath, m.id, "PARKED");
391
395
  if (parkedFile || m.status === 'parked') {
@@ -396,11 +400,6 @@ function buildCompletenessSet(basePath: string, milestones: MilestoneRow[]) {
396
400
  completeMilestoneIds.add(m.id);
397
401
  continue;
398
402
  }
399
- const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
400
- if (summaryFile) {
401
- completeMilestoneIds.add(m.id);
402
- continue;
403
- }
404
403
  }
405
404
  return { completeMilestoneIds, parkedMilestoneIds };
406
405
  }
@@ -429,18 +428,22 @@ async function buildRegistryAndFindActive(
429
428
  if (isGhostMilestone(basePath, m.id)) continue;
430
429
  }
431
430
 
432
- const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
433
-
434
- if (completeMilestoneIds.has(m.id) || (summaryFile !== null)) {
431
+ // DB-authoritative completeness (#4179): only trust completeMilestoneIds,
432
+ // which is itself derived from DB status. SUMMARY-file presence alone must
433
+ // not imply completion. The summary file may still be consulted below as a
434
+ // title source for legitimately-complete milestones whose DB row has no title.
435
+ if (completeMilestoneIds.has(m.id)) {
435
436
  let title = stripMilestonePrefix(m.title) || m.id;
436
- if (summaryFile && !m.title) {
437
- const summaryContent = await loadFile(summaryFile);
438
- if (summaryContent) {
439
- title = parseSummary(summaryContent).title || m.id;
437
+ if (!m.title) {
438
+ const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
439
+ if (summaryFile) {
440
+ const summaryContent = await loadFile(summaryFile);
441
+ if (summaryContent) {
442
+ title = parseSummary(summaryContent).title || m.id;
443
+ }
440
444
  }
441
445
  }
442
446
  registry.push({ id: m.id, title, status: 'complete' });
443
- completeMilestoneIds.add(m.id);
444
447
  continue;
445
448
  }
446
449
 
@@ -481,7 +484,14 @@ async function buildRegistryAndFindActive(
481
484
  const validationContent = validationFile ? await loadFile(validationFile) : null;
482
485
  const validationTerminal = validationContent ? isValidationTerminal(validationContent) : false;
483
486
 
484
- if (!validationTerminal || (validationTerminal && !summaryFile)) {
487
+ // DB-authoritative (#4179): completeness is already decided by
488
+ // completeMilestoneIds above. If we reached this branch, the DB says
489
+ // the milestone is NOT complete — so any SUMMARY file on disk is an
490
+ // orphan (crashed complete-milestone, partial merge, manual edit) and
491
+ // must not short-circuit this path. When validation is terminal, fall
492
+ // through to the default active-push below so `complete-milestone` can
493
+ // re-run idempotently.
494
+ if (!validationTerminal) {
485
495
  activeMilestone = { id: m.id, title };
486
496
  activeMilestoneSlices = slices;
487
497
  activeMilestoneFound = true;
@@ -630,13 +640,39 @@ function resolveSliceDependencies(activeMilestoneSlices: SliceRow[]): { activeSl
630
640
  }
631
641
  }
632
642
 
643
+ // First pass: find a slice with ALL dependencies satisfied (strict)
644
+ let bestFallback: SliceRow | null = null;
645
+ let bestFallbackSatisfied = -1;
646
+
633
647
  for (const s of activeMilestoneSlices) {
634
648
  if (isStatusDone(s.status)) continue;
635
649
  if (isDeferredStatus(s.status)) continue;
636
650
  if (s.depends.every(dep => doneSliceIds.has(dep))) {
637
651
  return { activeSlice: { id: s.id, title: s.title }, activeSliceRow: s };
638
652
  }
653
+ // Track the slice with the most satisfied dependencies as fallback
654
+ const satisfied = s.depends.filter(dep => doneSliceIds.has(dep)).length;
655
+ if (satisfied > bestFallbackSatisfied || (satisfied === bestFallbackSatisfied && !bestFallback)) {
656
+ bestFallback = s;
657
+ bestFallbackSatisfied = satisfied;
658
+ }
639
659
  }
660
+
661
+ // Fallback: if no slice has all deps met but there ARE incomplete non-deferred
662
+ // slices, pick the one with the most deps satisfied. This prevents hard-blocking
663
+ // when dependency metadata is stale (e.g. after reassessment added/removed slices)
664
+ // or when deps reference slices from previous milestones.
665
+ if (bestFallback) {
666
+ const unmet = bestFallback.depends.filter(dep => !doneSliceIds.has(dep));
667
+ logWarning("state",
668
+ `No slice has all deps satisfied — falling back to ${bestFallback.id} ` +
669
+ `(${bestFallbackSatisfied}/${bestFallback.depends.length} deps met, ` +
670
+ `unmet: ${unmet.join(", ")})`,
671
+ { mid: activeMilestoneSlices[0]?.milestone_id, sid: bestFallback.id },
672
+ );
673
+ return { activeSlice: { id: bestFallback.id, title: bestFallback.title }, activeSliceRow: bestFallback };
674
+ }
675
+
640
676
  return { activeSlice: null, activeSliceRow: null };
641
677
  }
642
678
 
@@ -684,7 +720,7 @@ async function reconcileSliceTasks(
684
720
  const summaryPath = resolveTaskFile(basePath, milestoneId, sliceId, t.id, "SUMMARY");
685
721
  if (summaryPath && existsSync(summaryPath)) {
686
722
  try {
687
- updateTaskStatus(milestoneId, sliceId, t.id, "complete");
723
+ updateTaskStatus(milestoneId, sliceId, t.id, "complete", new Date().toISOString());
688
724
  logWarning("reconcile", `task ${milestoneId}/${sliceId}/${t.id} status reconciled from "${t.status}" to "complete" (#2514)`, { mid: milestoneId, sid: sliceId, tid: t.id });
689
725
  reconciled = true;
690
726
  } catch (e) {
@@ -1431,12 +1467,32 @@ export async function _deriveStateImpl(basePath: string): Promise<GSDState> {
1431
1467
  };
1432
1468
  }
1433
1469
  } else {
1470
+ let bestFallbackLegacy: { id: string; title: string; depends: string[] } | null = null;
1471
+ let bestFallbackLegacySatisfied = -1;
1472
+
1434
1473
  for (const s of activeRoadmap.slices) {
1435
1474
  if (s.done) continue;
1436
1475
  if (s.depends.every(dep => doneSliceIds.has(dep))) {
1437
1476
  activeSlice = { id: s.id, title: s.title };
1438
1477
  break;
1439
1478
  }
1479
+ // Track best fallback
1480
+ const satisfied = s.depends.filter(dep => doneSliceIds.has(dep)).length;
1481
+ if (satisfied > bestFallbackLegacySatisfied) {
1482
+ bestFallbackLegacy = s;
1483
+ bestFallbackLegacySatisfied = satisfied;
1484
+ }
1485
+ }
1486
+
1487
+ // Fallback: if no slice has all deps met, pick the one with the most deps satisfied
1488
+ if (!activeSlice && bestFallbackLegacy) {
1489
+ const unmet = bestFallbackLegacy.depends.filter(dep => !doneSliceIds.has(dep));
1490
+ logWarning("state",
1491
+ `No slice has all deps satisfied — falling back to ${bestFallbackLegacy.id} ` +
1492
+ `(${bestFallbackLegacySatisfied}/${bestFallbackLegacy.depends.length} deps met, ` +
1493
+ `unmet: ${unmet.join(", ")})`,
1494
+ );
1495
+ activeSlice = { id: bestFallbackLegacy.id, title: bestFallbackLegacy.title };
1440
1496
  }
1441
1497
  }
1442
1498
 
@@ -688,8 +688,8 @@ test("autoLoop exits on terminal blocked state", async (t) => {
688
688
 
689
689
  assert.ok(deps.callLog.includes("deriveState"), "should have derived state");
690
690
  assert.ok(
691
- deps.callLog.includes("stopAuto"),
692
- "should have called stopAuto for blocked state",
691
+ deps.callLog.includes("pauseAuto"),
692
+ "should have called pauseAuto for blocked state",
693
693
  );
694
694
  assert.ok(
695
695
  !deps.callLog.includes("resolveDispatch"),
@@ -0,0 +1,53 @@
1
+ // GSD-2 — Tests for step-mode completion messages in auto-post-unit
2
+
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+
6
+ import { buildStepCompleteMessage, STEP_COMPLETE_FALLBACK_MESSAGE } from "../auto-post-unit.ts";
7
+ import type { GSDState } from "../types.ts";
8
+
9
+ function makeState(overrides: Partial<GSDState>): GSDState {
10
+ return {
11
+ activeMilestone: null,
12
+ activeSlice: null,
13
+ activeTask: null,
14
+ phase: "executing",
15
+ recentDecisions: [],
16
+ blockers: [],
17
+ nextAction: "",
18
+ registry: [],
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ test("buildStepCompleteMessage: milestone complete surfaces review guidance", () => {
24
+ const msg = buildStepCompleteMessage(makeState({ phase: "complete" }));
25
+ assert.match(msg, /milestone finished/);
26
+ assert.match(msg, /\/gsd status/);
27
+ assert.doesNotMatch(msg, /Next:/);
28
+ });
29
+
30
+ test("buildStepCompleteMessage: mid-flight step includes next unit label and /clear hint", () => {
31
+ const state = makeState({
32
+ phase: "executing",
33
+ activeSlice: { id: "S01", title: "Core" },
34
+ activeTask: { id: "T03", title: "Wire notify" },
35
+ });
36
+ const msg = buildStepCompleteMessage(state);
37
+ assert.match(msg, /Next: Execute T03: Wire notify/);
38
+ assert.match(msg, /\/clear/);
39
+ assert.match(msg, /\/gsd to continue/);
40
+ });
41
+
42
+ test("buildStepCompleteMessage: unknown phase falls back to generic continue label", () => {
43
+ // Cast to bypass Phase union so we exercise the default branch of describeNextUnit.
44
+ const state = makeState({ phase: "totally-unknown" as unknown as GSDState["phase"] });
45
+ const msg = buildStepCompleteMessage(state);
46
+ assert.match(msg, /Next: Continue/);
47
+ assert.match(msg, /\/clear/);
48
+ });
49
+
50
+ test("STEP_COMPLETE_FALLBACK_MESSAGE: used when deriveState throws, still points users at /clear + /gsd", () => {
51
+ assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/clear/);
52
+ assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/gsd/);
53
+ });