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.
- package/dist/cli-web-branch.d.ts +4 -3
- package/dist/cli-web-branch.js +10 -7
- package/dist/cli.js +99 -206
- package/dist/logo.d.ts +1 -1
- package/dist/logo.js +1 -1
- package/dist/onboarding.js +59 -53
- package/dist/resource-loader.js +2 -2
- package/dist/resources/extensions/gsd/auto/phases.js +15 -9
- package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +41 -1
- package/dist/resources/extensions/gsd/auto-start.js +3 -0
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
- package/dist/resources/extensions/gsd/auto-verification.js +88 -3
- package/dist/resources/extensions/gsd/auto.js +29 -8
- package/dist/resources/extensions/gsd/commands-handlers.js +8 -2
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/dist/resources/extensions/gsd/notification-widget.js +2 -2
- package/dist/resources/extensions/gsd/state.js +61 -14
- package/dist/update-check.d.ts +1 -0
- package/dist/update-check.js +13 -5
- package/dist/update-cmd.js +4 -3
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -2
- package/packages/pi-ai/dist/utils/overflow.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/overflow.js +12 -0
- package/packages/pi-ai/dist/utils/overflow.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts +2 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.js +50 -0
- package/packages/pi-ai/dist/utils/tests/overflow.test.js.map +1 -0
- package/packages/pi-ai/src/utils/overflow.ts +14 -1
- package/packages/pi-ai/src/utils/tests/overflow.test.ts +58 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +138 -0
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction/utils.js +5 -5
- package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.js +45 -0
- package/packages/pi-coding-agent/dist/core/compaction-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +21 -4
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +11 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +157 -0
- package/packages/pi-coding-agent/src/core/compaction/utils.ts +5 -5
- package/packages/pi-coding-agent/src/core/compaction-utils.test.ts +50 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -4
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +11 -3
- package/packages/pi-tui/dist/__tests__/tui.test.js +60 -1
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
- package/packages/pi-tui/dist/tui.d.ts +8 -0
- package/packages/pi-tui/dist/tui.d.ts.map +1 -1
- package/packages/pi-tui/dist/tui.js +32 -3
- package/packages/pi-tui/dist/tui.js.map +1 -1
- package/packages/pi-tui/src/__tests__/tui.test.ts +76 -1
- package/packages/pi-tui/src/tui.ts +31 -3
- package/src/resources/extensions/gsd/auto/phases.ts +22 -9
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +47 -1
- package/src/resources/extensions/gsd/auto-start.ts +3 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
- package/src/resources/extensions/gsd/auto-verification.ts +98 -3
- package/src/resources/extensions/gsd/auto.ts +31 -14
- package/src/resources/extensions/gsd/commands-handlers.ts +8 -2
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/src/resources/extensions/gsd/notification-widget.ts +2 -2
- package/src/resources/extensions/gsd/state.ts +71 -15
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
- /package/dist/web/standalone/.next/static/{r6AvNu-aMwn4nwqjHqAfw → ASJ2RGD7E1iiUYzA0xT2i}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{r6AvNu-aMwn4nwqjHqAfw → ASJ2RGD7E1iiUYzA0xT2i}/_ssgManifest.js +0 -0
package/dist/resource-loader.js
CHANGED
|
@@ -358,7 +358,7 @@ function reconcileMergedNodeModules(agentNodeModules, hoisted, internal) {
|
|
|
358
358
|
if (entry.name.startsWith('.'))
|
|
359
359
|
continue;
|
|
360
360
|
try {
|
|
361
|
-
symlinkSync(join(hoisted, entry.name), join(agentNodeModules, entry.name));
|
|
361
|
+
symlinkSync(join(hoisted, entry.name), join(agentNodeModules, entry.name), 'junction');
|
|
362
362
|
linkedCount++;
|
|
363
363
|
}
|
|
364
364
|
catch { /* skip individual */ }
|
|
@@ -382,7 +382,7 @@ function reconcileMergedNodeModules(agentNodeModules, hoisted, internal) {
|
|
|
382
382
|
}
|
|
383
383
|
catch { /* didn't exist — will create below */ }
|
|
384
384
|
try {
|
|
385
|
-
symlinkSync(join(internal, entry.name), link);
|
|
385
|
+
symlinkSync(join(internal, entry.name), link, 'junction');
|
|
386
386
|
linkedCount++;
|
|
387
387
|
}
|
|
388
388
|
catch { /* skip individual */ }
|
|
@@ -320,10 +320,13 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
320
320
|
}
|
|
321
321
|
else if (state.phase === "blocked") {
|
|
322
322
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
deps.
|
|
323
|
+
// Pause instead of hard-stop so the session is resumable with `/gsd auto`.
|
|
324
|
+
// Hard-stop here was causing premature termination when slice dependencies
|
|
325
|
+
// were temporarily unresolvable (e.g. after reassessment added new slices).
|
|
326
|
+
await deps.pauseAuto(ctx, pi);
|
|
327
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
|
|
328
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
|
|
329
|
+
deps.logCmuxEvent(prefs, blockerMsg, "warning");
|
|
327
330
|
}
|
|
328
331
|
else {
|
|
329
332
|
const ids = incomplete.map((m) => m.id).join(", ");
|
|
@@ -392,13 +395,16 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
392
395
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "milestone-complete", milestoneId: mid } });
|
|
393
396
|
return { action: "break", reason: "milestone-complete" };
|
|
394
397
|
}
|
|
395
|
-
// Terminal: blocked
|
|
398
|
+
// Terminal: blocked — pause instead of hard-stop so the session is resumable.
|
|
396
399
|
if (state.phase === "blocked") {
|
|
397
400
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
deps.
|
|
401
|
+
if (s.currentUnit) {
|
|
402
|
+
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
403
|
+
}
|
|
404
|
+
await deps.pauseAuto(ctx, pi);
|
|
405
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
|
|
406
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
|
|
407
|
+
deps.logCmuxEvent(prefs, blockerMsg, "warning");
|
|
402
408
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
403
409
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
|
|
404
410
|
return { action: "break", reason: "blocked" };
|
|
@@ -216,7 +216,12 @@ export const DISPATCH_RULES = [
|
|
|
216
216
|
{
|
|
217
217
|
name: "reassess-roadmap (post-completion)",
|
|
218
218
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
219
|
-
if (prefs?.phases?.skip_reassess
|
|
219
|
+
if (prefs?.phases?.skip_reassess)
|
|
220
|
+
return null;
|
|
221
|
+
// Default reassess_after_slice to true — reassessment after slice completion
|
|
222
|
+
// is essential for roadmap integrity. Opt-out via explicit `false`.
|
|
223
|
+
const reassessEnabled = prefs?.phases?.reassess_after_slice ?? true;
|
|
224
|
+
if (!reassessEnabled)
|
|
220
225
|
return null;
|
|
221
226
|
const needsReassess = await checkNeedsReassessment(basePath, mid, state);
|
|
222
227
|
if (!needsReassess)
|
|
@@ -710,11 +715,14 @@ export async function resolveDispatch(ctx) {
|
|
|
710
715
|
return result;
|
|
711
716
|
}
|
|
712
717
|
}
|
|
713
|
-
// No rule matched — unhandled phase
|
|
718
|
+
// No rule matched — unhandled phase.
|
|
719
|
+
// Use level "warning" so the loop pauses (resumable) instead of hard-stopping.
|
|
720
|
+
// Hard-stop here was causing premature termination for transient phase gaps
|
|
721
|
+
// (e.g. after reassessment modifies the roadmap and state needs re-derivation).
|
|
714
722
|
return {
|
|
715
723
|
action: "stop",
|
|
716
724
|
reason: `Unhandled phase "${ctx.state.phase}" — run /gsd doctor to diagnose.`,
|
|
717
|
-
level: "
|
|
725
|
+
level: "warning",
|
|
718
726
|
matchedRule: "<no-match>",
|
|
719
727
|
};
|
|
720
728
|
}
|
|
@@ -67,6 +67,7 @@ const LIFECYCLE_ONLY_UNITS = new Set([
|
|
|
67
67
|
"replan-slice", "complete-slice", "run-uat",
|
|
68
68
|
"reassess-roadmap", "rewrite-docs",
|
|
69
69
|
]);
|
|
70
|
+
import { describeNextUnit, } from "./auto-dashboard.js";
|
|
70
71
|
import { existsSync, unlinkSync } from "node:fs";
|
|
71
72
|
import { join } from "node:path";
|
|
72
73
|
import { _resetHasChangesCache } from "./native-git-bridge.js";
|
|
@@ -179,6 +180,15 @@ export function detectRogueFileWrites(unitType, unitId, basePath) {
|
|
|
179
180
|
}
|
|
180
181
|
return rogues;
|
|
181
182
|
}
|
|
183
|
+
export const STEP_COMPLETE_FALLBACK_MESSAGE = "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).";
|
|
184
|
+
export function buildStepCompleteMessage(nextState) {
|
|
185
|
+
if (nextState.phase === "complete") {
|
|
186
|
+
return "Step complete — milestone finished. Run /gsd status to review, or start the next milestone.";
|
|
187
|
+
}
|
|
188
|
+
const next = describeNextUnit(nextState);
|
|
189
|
+
return `Step complete. Next: ${next.label}\n`
|
|
190
|
+
+ `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
|
|
191
|
+
}
|
|
182
192
|
/**
|
|
183
193
|
* Pre-verification processing: parallel worker signal check, cache invalidation,
|
|
184
194
|
* auto-commit, doctor run, state rebuild, worktree sync, artifact verification.
|
|
@@ -509,6 +519,26 @@ export async function postUnitPreVerification(pctx, opts) {
|
|
|
509
519
|
const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
|
|
510
520
|
s.verificationRetryCount.set(retryKey, attempt);
|
|
511
521
|
if (attempt > MAX_VERIFICATION_RETRIES) {
|
|
522
|
+
// #4175: For complete-milestone, a blocker placeholder is harmful —
|
|
523
|
+
// the stub SUMMARY has no recovery value (milestone is terminal),
|
|
524
|
+
// it does not update DB status (so deriveState never advances),
|
|
525
|
+
// and it fools stopAuto's presence check into merging a milestone
|
|
526
|
+
// that was never legitimately completed. Pause auto-mode with a
|
|
527
|
+
// clear single failure signal and preserve the worktree branch.
|
|
528
|
+
if (s.currentUnit.type === "complete-milestone") {
|
|
529
|
+
debugLog("postUnit", {
|
|
530
|
+
phase: "artifact-verify-pause-complete-milestone",
|
|
531
|
+
unitType: s.currentUnit.type,
|
|
532
|
+
unitId: s.currentUnit.id,
|
|
533
|
+
attempt,
|
|
534
|
+
maxRetries: MAX_VERIFICATION_RETRIES,
|
|
535
|
+
});
|
|
536
|
+
s.verificationRetryCount.delete(retryKey);
|
|
537
|
+
s.pendingVerificationRetry = null;
|
|
538
|
+
ctx.ui.notify(`Milestone ${s.currentUnit.id} verification failed after ${MAX_VERIFICATION_RETRIES} retries — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`, "error");
|
|
539
|
+
await pauseAuto(ctx, pi);
|
|
540
|
+
return "dispatched";
|
|
541
|
+
}
|
|
512
542
|
// Retries exhausted — write a blocker placeholder so the pipeline
|
|
513
543
|
// can advance past this stuck unit (#2653).
|
|
514
544
|
debugLog("postUnit", {
|
|
@@ -836,8 +866,18 @@ export async function postUnitPostVerification(pctx) {
|
|
|
836
866
|
debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
|
|
837
867
|
}
|
|
838
868
|
}
|
|
839
|
-
// Step mode → show wizard instead of dispatch
|
|
869
|
+
// Step mode → show wizard instead of dispatch.
|
|
870
|
+
// Without this notify(), /gsd in step mode finishes a unit and silently
|
|
871
|
+
// exits the loop, leaving the user with no hint to /clear and /gsd again.
|
|
840
872
|
if (s.stepMode) {
|
|
873
|
+
try {
|
|
874
|
+
const nextState = await deriveState(s.basePath);
|
|
875
|
+
ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
|
|
876
|
+
}
|
|
877
|
+
catch (e) {
|
|
878
|
+
debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
|
|
879
|
+
ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
|
|
880
|
+
}
|
|
841
881
|
return "step-wizard";
|
|
842
882
|
}
|
|
843
883
|
return "continue";
|
|
@@ -632,6 +632,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
632
632
|
}
|
|
633
633
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
634
634
|
ctx.ui.setFooter(hideFooter);
|
|
635
|
+
// Hide gsd-health during AUTO — gsd-progress is the single source of truth
|
|
636
|
+
// for last-commit / cost / health signal while auto is running.
|
|
637
|
+
ctx.ui.setWidget("gsd-health", undefined);
|
|
635
638
|
const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
|
|
636
639
|
const pendingCount = (state.registry ?? []).filter((m) => m.status !== "complete" && m.status !== "parked").length;
|
|
637
640
|
const scopeMsg = pendingCount > 1
|
|
@@ -156,6 +156,19 @@ export async function recoverTimedOutUnit(ctx, pi, unitType, unitId, reason, rct
|
|
|
156
156
|
ctx.ui.notify(`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to produce ${expected} (attempt ${attemptNumber}, session ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`, "warning");
|
|
157
157
|
return "recovered";
|
|
158
158
|
}
|
|
159
|
+
// #4175: For complete-milestone, never write a blocker placeholder — a stub
|
|
160
|
+
// SUMMARY has no recovery value (milestone is terminal), it does not update
|
|
161
|
+
// DB status, and downstream merge paths can treat the stub as a legitimate
|
|
162
|
+
// completion signal. Pause instead so the worktree branch is preserved.
|
|
163
|
+
if (unitType === "complete-milestone") {
|
|
164
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
|
|
165
|
+
phase: "paused",
|
|
166
|
+
recoveryAttempts: recoveryAttempts + 1,
|
|
167
|
+
lastRecoveryReason: reason,
|
|
168
|
+
});
|
|
169
|
+
ctx.ui.notify(`Milestone ${unitId} ${reason}-recovery exhausted ${maxRecoveryAttempts} attempt(s) — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`, "error");
|
|
170
|
+
return "paused";
|
|
171
|
+
}
|
|
159
172
|
// Retries exhausted — write a blocker placeholder and advance the pipeline
|
|
160
173
|
// instead of silently stalling.
|
|
161
174
|
const placeholder = writeBlockerPlaceholder(unitType, unitId, basePath, `${reason} recovery exhausted ${maxRecoveryAttempts} attempts without producing the artifact.`);
|
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
* checks the result and handles control flow.
|
|
11
11
|
*/
|
|
12
12
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { resolveSlicePath } from "./paths.js";
|
|
13
|
+
import { resolveSlicePath, resolveMilestoneFile } from "./paths.js";
|
|
14
14
|
import { parseUnitId } from "./unit-id.js";
|
|
15
|
-
import { isDbAvailable, getTask, getSliceTasks } from "./gsd-db.js";
|
|
15
|
+
import { isDbAvailable, getTask, getSliceTasks, getMilestoneSlices } from "./gsd-db.js";
|
|
16
16
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
17
|
+
import { extractVerdict } from "./verdict-parser.js";
|
|
18
|
+
import { isClosedStatus } from "./status-guards.js";
|
|
19
|
+
import { loadFile } from "./files.js";
|
|
20
|
+
import { parseRoadmap } from "./parsers-legacy.js";
|
|
21
|
+
import { isMilestoneComplete } from "./state.js";
|
|
17
22
|
import { runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit, } from "./verification-gate.js";
|
|
18
23
|
import { writeVerificationJSON } from "./verification-evidence.js";
|
|
19
24
|
import { logWarning } from "./workflow-logger.js";
|
|
@@ -22,6 +27,80 @@ import { join } from "node:path";
|
|
|
22
27
|
function isInfraVerificationFailure(stderr) {
|
|
23
28
|
return /\b(ENOENT|ENOTFOUND|ETIMEDOUT|ECONNRESET|EAI_AGAIN|spawn\s+\S+\s+ENOENT|command not found)\b/i.test(stderr);
|
|
24
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Post-unit guard for `validate-milestone` units (#4094).
|
|
32
|
+
*
|
|
33
|
+
* When validate-milestone writes verdict=needs-remediation, the agent is
|
|
34
|
+
* expected to also call gsd_reassess_roadmap in the same turn to add
|
|
35
|
+
* remediation slices. If they don't, the state machine re-derives
|
|
36
|
+
* `phase: validating-milestone` indefinitely (all slices still complete +
|
|
37
|
+
* verdict still needs-remediation), wasting ~3 dispatches before the stuck
|
|
38
|
+
* detector fires.
|
|
39
|
+
*
|
|
40
|
+
* This guard fires immediately on the first occurrence: if VALIDATION.md
|
|
41
|
+
* verdict is needs-remediation and no incomplete slices exist for the
|
|
42
|
+
* milestone, pause the auto-loop with a clear blocker.
|
|
43
|
+
*/
|
|
44
|
+
async function runValidateMilestonePostCheck(vctx, pauseAuto) {
|
|
45
|
+
const { s, ctx, pi } = vctx;
|
|
46
|
+
if (!s.currentUnit)
|
|
47
|
+
return "continue";
|
|
48
|
+
const { milestone: mid } = parseUnitId(s.currentUnit.id);
|
|
49
|
+
if (!mid)
|
|
50
|
+
return "continue";
|
|
51
|
+
const validationFile = resolveMilestoneFile(s.basePath, mid, "VALIDATION");
|
|
52
|
+
if (!validationFile)
|
|
53
|
+
return "continue";
|
|
54
|
+
const validationContent = await loadFile(validationFile);
|
|
55
|
+
if (!validationContent)
|
|
56
|
+
return "continue";
|
|
57
|
+
const verdict = extractVerdict(validationContent);
|
|
58
|
+
if (verdict !== "needs-remediation")
|
|
59
|
+
return "continue";
|
|
60
|
+
const incompleteSliceCount = await countIncompleteSlices(s.basePath, mid);
|
|
61
|
+
// If any non-closed slices exist, the agent successfully queued remediation
|
|
62
|
+
// work — proceed normally. The state machine will execute those slices and
|
|
63
|
+
// re-validate per the #3596/#3670 fix.
|
|
64
|
+
if (incompleteSliceCount > 0)
|
|
65
|
+
return "continue";
|
|
66
|
+
ctx.ui.notify(`Milestone ${mid} validation returned verdict=needs-remediation but no remediation slices were added. Pausing for human review.`, "error");
|
|
67
|
+
process.stderr.write(`validate-milestone: pausing — verdict=needs-remediation with no incomplete slices for ${mid}. ` +
|
|
68
|
+
`The agent must call gsd_reassess_roadmap to add remediation slices before re-validation.\n`);
|
|
69
|
+
await pauseAuto(ctx, pi);
|
|
70
|
+
return "pause";
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Count slices for a milestone that are not in a closed status.
|
|
74
|
+
* DB-backed projects are authoritative (#4094 peer review); falls back to
|
|
75
|
+
* roadmap parsing only when the DB is unavailable.
|
|
76
|
+
*/
|
|
77
|
+
async function countIncompleteSlices(basePath, milestoneId) {
|
|
78
|
+
if (isDbAvailable()) {
|
|
79
|
+
const slices = getMilestoneSlices(milestoneId);
|
|
80
|
+
if (slices.length === 0) {
|
|
81
|
+
// No DB rows — treat as "unknown", do not pause.
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
return slices.filter((slice) => !isClosedStatus(slice.status)).length;
|
|
85
|
+
}
|
|
86
|
+
// Filesystem fallback: parse the roadmap markdown.
|
|
87
|
+
try {
|
|
88
|
+
const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
|
|
89
|
+
if (!roadmapFile)
|
|
90
|
+
return 1;
|
|
91
|
+
const roadmapContent = await loadFile(roadmapFile);
|
|
92
|
+
if (!roadmapContent)
|
|
93
|
+
return 1;
|
|
94
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
95
|
+
if (roadmap.slices.length === 0)
|
|
96
|
+
return 1;
|
|
97
|
+
return isMilestoneComplete(roadmap) ? 0 : 1;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Parsing failures should not cause false-positive pauses.
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
25
104
|
/**
|
|
26
105
|
* Run the verification gate for the current execute-task unit.
|
|
27
106
|
* Returns:
|
|
@@ -31,7 +110,13 @@ function isInfraVerificationFailure(stderr) {
|
|
|
31
110
|
*/
|
|
32
111
|
export async function runPostUnitVerification(vctx, pauseAuto) {
|
|
33
112
|
const { s, ctx, pi } = vctx;
|
|
34
|
-
if (!s.currentUnit
|
|
113
|
+
if (!s.currentUnit) {
|
|
114
|
+
return "continue";
|
|
115
|
+
}
|
|
116
|
+
if (s.currentUnit.type === "validate-milestone") {
|
|
117
|
+
return await runValidateMilestonePostCheck(vctx, pauseAuto);
|
|
118
|
+
}
|
|
119
|
+
if (s.currentUnit.type !== "execute-task") {
|
|
35
120
|
return "continue";
|
|
36
121
|
}
|
|
37
122
|
try {
|
|
@@ -55,7 +55,7 @@ import { initRegistry, convertDispatchRules } from "./rule-registry.js";
|
|
|
55
55
|
import { emitJournalEvent as _emitJournalEvent } from "./journal.js";
|
|
56
56
|
import { updateProgressWidget as _updateProgressWidget, updateSliceProgressCache, clearSliceProgressCache, hideFooter, } from "./auto-dashboard.js";
|
|
57
57
|
import { registerSigtermHandler as _registerSigtermHandler, deregisterSigtermHandler as _deregisterSigtermHandler, } from "./auto-supervisor.js";
|
|
58
|
-
import { isDbAvailable } from "./gsd-db.js";
|
|
58
|
+
import { isDbAvailable, getMilestone } from "./gsd-db.js";
|
|
59
59
|
import { countPendingCaptures } from "./captures.js";
|
|
60
60
|
import { clearCmuxSidebar, logCmuxEvent, syncCmuxSidebar } from "../cmux/index.js";
|
|
61
61
|
// ── Extracted modules ──────────────────────────────────────────────────────
|
|
@@ -63,6 +63,7 @@ import { startUnitSupervision } from "./auto-timers.js";
|
|
|
63
63
|
import { runPostUnitVerification } from "./auto-verification.js";
|
|
64
64
|
import { postUnitPreVerification, postUnitPostVerification, } from "./auto-post-unit.js";
|
|
65
65
|
import { bootstrapAutoSession, openProjectDbIfPresent } from "./auto-start.js";
|
|
66
|
+
import { initHealthWidget } from "./health-widget.js";
|
|
66
67
|
import { autoLoop, resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSessionSwitchInFlight } from "./auto-loop.js";
|
|
67
68
|
import { WorktreeResolver, } from "./worktree-resolver.js";
|
|
68
69
|
import { reorderForCaching } from "./prompt-ordering.js";
|
|
@@ -397,6 +398,8 @@ function handleLostSessionLock(ctx, lockStatus) {
|
|
|
397
398
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
398
399
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
399
400
|
ctx?.ui.setFooter(undefined);
|
|
401
|
+
if (ctx)
|
|
402
|
+
initHealthWidget(ctx);
|
|
400
403
|
}
|
|
401
404
|
/**
|
|
402
405
|
* Lightweight cleanup after autoLoop exits via step-wizard break.
|
|
@@ -431,6 +434,7 @@ function cleanupAfterLoopExit(ctx) {
|
|
|
431
434
|
ctx.ui.setStatus("gsd-auto", undefined);
|
|
432
435
|
ctx.ui.setWidget("gsd-progress", undefined);
|
|
433
436
|
ctx.ui.setFooter(undefined);
|
|
437
|
+
initHealthWidget(ctx);
|
|
434
438
|
}
|
|
435
439
|
// Restore CWD out of worktree back to original project root
|
|
436
440
|
if (s.originalBasePath) {
|
|
@@ -501,17 +505,30 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
501
505
|
? { notify: ctx.ui.notify.bind(ctx.ui) }
|
|
502
506
|
: { notify: () => { } };
|
|
503
507
|
const resolver = buildResolver();
|
|
504
|
-
// Check if the milestone is complete
|
|
508
|
+
// Check if the milestone is complete. DB status is the authoritative
|
|
509
|
+
// signal — only a successful gsd_complete_milestone call flips it to
|
|
510
|
+
// "complete" (tools/complete-milestone.ts). SUMMARY file presence is
|
|
511
|
+
// NOT sufficient: a blocker placeholder stub or a partial write can
|
|
512
|
+
// leave a file behind without the milestone actually being done,
|
|
513
|
+
// which previously caused stopAuto to merge a failed milestone and
|
|
514
|
+
// emit a misleading metadata-only merge warning (#4175).
|
|
515
|
+
// DB-unavailable projects fall back to SUMMARY-file presence.
|
|
505
516
|
let milestoneComplete = false;
|
|
506
517
|
try {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const wtSummaryPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "SUMMARY");
|
|
511
|
-
milestoneComplete = wtSummaryPath !== null;
|
|
518
|
+
if (isDbAvailable()) {
|
|
519
|
+
const dbRow = getMilestone(s.currentMilestoneId);
|
|
520
|
+
milestoneComplete = dbRow?.status === "complete";
|
|
512
521
|
}
|
|
513
522
|
else {
|
|
514
|
-
|
|
523
|
+
const summaryPath = resolveMilestoneFile(s.originalBasePath || s.basePath, s.currentMilestoneId, "SUMMARY");
|
|
524
|
+
if (!summaryPath) {
|
|
525
|
+
// Also check in the worktree path (SUMMARY may not be synced yet)
|
|
526
|
+
const wtSummaryPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "SUMMARY");
|
|
527
|
+
milestoneComplete = wtSummaryPath !== null;
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
milestoneComplete = true;
|
|
531
|
+
}
|
|
515
532
|
}
|
|
516
533
|
}
|
|
517
534
|
catch (err) {
|
|
@@ -676,6 +693,8 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
676
693
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
677
694
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
678
695
|
ctx?.ui.setFooter(undefined);
|
|
696
|
+
if (ctx)
|
|
697
|
+
initHealthWidget(ctx);
|
|
679
698
|
restoreProjectRootEnv();
|
|
680
699
|
restoreMilestoneLockEnv();
|
|
681
700
|
// Reset all session state in one call
|
|
@@ -762,6 +781,8 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
|
|
|
762
781
|
ctx?.ui.setStatus("gsd-auto", "paused");
|
|
763
782
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
764
783
|
ctx?.ui.setFooter(undefined);
|
|
784
|
+
if (ctx)
|
|
785
|
+
initHealthWidget(ctx);
|
|
765
786
|
const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
|
|
766
787
|
ctx?.ui.notify(`${s.stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`, "info");
|
|
767
788
|
}
|
|
@@ -17,6 +17,11 @@ import { projectRoot } from "./commands/context.js";
|
|
|
17
17
|
import { loadPrompt } from "./prompt-loader.js";
|
|
18
18
|
const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/gsd-pi/latest";
|
|
19
19
|
const UPDATE_FETCH_TIMEOUT_MS = 5000;
|
|
20
|
+
function resolveInstallCommand(pkg) {
|
|
21
|
+
if ('bun' in process.versions)
|
|
22
|
+
return `bun add -g ${pkg}`;
|
|
23
|
+
return `npm install -g ${pkg}`;
|
|
24
|
+
}
|
|
20
25
|
async function fetchLatestVersionForCommand() {
|
|
21
26
|
const controller = new AbortController();
|
|
22
27
|
const timeout = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
|
|
@@ -344,13 +349,14 @@ export async function handleUpdate(ctx) {
|
|
|
344
349
|
return;
|
|
345
350
|
}
|
|
346
351
|
ctx.ui.notify(`Updating: v${current} → v${latest}...`, "info");
|
|
352
|
+
const installCmd = resolveInstallCommand(`${NPM_PACKAGE}@latest`);
|
|
347
353
|
try {
|
|
348
|
-
execSync(
|
|
354
|
+
execSync(installCmd, {
|
|
349
355
|
stdio: ["ignore", "pipe", "ignore"],
|
|
350
356
|
});
|
|
351
357
|
ctx.ui.notify(`Updated to v${latest}. Restart your GSD session to use the new version.`, "info");
|
|
352
358
|
}
|
|
353
359
|
catch {
|
|
354
|
-
ctx.ui.notify(`Update failed. Try manually:
|
|
360
|
+
ctx.ui.notify(`Update failed. Try manually: ${installCmd}`, "error");
|
|
355
361
|
}
|
|
356
362
|
}
|
|
@@ -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: `
|
|
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
|
|
3
|
+
// the most recent notification message. Refreshes every 30 seconds.
|
|
4
4
|
// Widget key: "gsd-notifications", placement: "belowEditor"
|
|
5
5
|
import { getUnreadCount, onNotificationStoreChange } from "./notification-store.js";
|
|
6
6
|
import { formattedShortcutPair } from "./shortcut-defs.js";
|
|
@@ -12,7 +12,7 @@ export function buildNotificationWidgetLines() {
|
|
|
12
12
|
return [` 🔔 Notifications: ${unread} unread (${formattedShortcutPair("notifications")})`];
|
|
13
13
|
}
|
|
14
14
|
// ─── Widget init ────────────────────────────────────────────────────────
|
|
15
|
-
const REFRESH_INTERVAL_MS =
|
|
15
|
+
const REFRESH_INTERVAL_MS = 30_000;
|
|
16
16
|
/**
|
|
17
17
|
* Initialize the always-on notification widget (belowEditor).
|
|
18
18
|
* Call once from session_start after the notification store is initialized.
|
|
@@ -312,6 +312,10 @@ function reconcileDiskToDb(basePath) {
|
|
|
312
312
|
function buildCompletenessSet(basePath, milestones) {
|
|
313
313
|
const completeMilestoneIds = new Set();
|
|
314
314
|
const parkedMilestoneIds = new Set();
|
|
315
|
+
// DB-authoritative: a milestone is only "complete" when its DB row says so.
|
|
316
|
+
// SUMMARY-file presence is NOT a completion signal here — an orphan SUMMARY
|
|
317
|
+
// (crashed complete-milestone turn, partial merge, manual edit) must not
|
|
318
|
+
// flip derived state to complete and cascade into a false auto-merge (#4179).
|
|
315
319
|
for (const m of milestones) {
|
|
316
320
|
const parkedFile = resolveMilestoneFile(basePath, m.id, "PARKED");
|
|
317
321
|
if (parkedFile || m.status === 'parked') {
|
|
@@ -322,11 +326,6 @@ function buildCompletenessSet(basePath, milestones) {
|
|
|
322
326
|
completeMilestoneIds.add(m.id);
|
|
323
327
|
continue;
|
|
324
328
|
}
|
|
325
|
-
const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
|
|
326
|
-
if (summaryFile) {
|
|
327
|
-
completeMilestoneIds.add(m.id);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
329
|
}
|
|
331
330
|
return { completeMilestoneIds, parkedMilestoneIds };
|
|
332
331
|
}
|
|
@@ -347,17 +346,22 @@ async function buildRegistryAndFindActive(basePath, milestones, completeMileston
|
|
|
347
346
|
if (isGhostMilestone(basePath, m.id))
|
|
348
347
|
continue;
|
|
349
348
|
}
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
// DB-authoritative completeness (#4179): only trust completeMilestoneIds,
|
|
350
|
+
// which is itself derived from DB status. SUMMARY-file presence alone must
|
|
351
|
+
// not imply completion. The summary file may still be consulted below as a
|
|
352
|
+
// title source for legitimately-complete milestones whose DB row has no title.
|
|
353
|
+
if (completeMilestoneIds.has(m.id)) {
|
|
352
354
|
let title = stripMilestonePrefix(m.title) || m.id;
|
|
353
|
-
if (
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
-
|
|
355
|
+
if (!m.title) {
|
|
356
|
+
const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
|
|
357
|
+
if (summaryFile) {
|
|
358
|
+
const summaryContent = await loadFile(summaryFile);
|
|
359
|
+
if (summaryContent) {
|
|
360
|
+
title = parseSummary(summaryContent).title || m.id;
|
|
361
|
+
}
|
|
357
362
|
}
|
|
358
363
|
}
|
|
359
364
|
registry.push({ id: m.id, title, status: 'complete' });
|
|
360
|
-
completeMilestoneIds.add(m.id);
|
|
361
365
|
continue;
|
|
362
366
|
}
|
|
363
367
|
const allSlicesDone = slices.length > 0 && slices.every(s => isStatusDone(s.status));
|
|
@@ -391,7 +395,14 @@ async function buildRegistryAndFindActive(basePath, milestones, completeMileston
|
|
|
391
395
|
const validationFile = resolveMilestoneFile(basePath, m.id, "VALIDATION");
|
|
392
396
|
const validationContent = validationFile ? await loadFile(validationFile) : null;
|
|
393
397
|
const validationTerminal = validationContent ? isValidationTerminal(validationContent) : false;
|
|
394
|
-
|
|
398
|
+
// DB-authoritative (#4179): completeness is already decided by
|
|
399
|
+
// completeMilestoneIds above. If we reached this branch, the DB says
|
|
400
|
+
// the milestone is NOT complete — so any SUMMARY file on disk is an
|
|
401
|
+
// orphan (crashed complete-milestone, partial merge, manual edit) and
|
|
402
|
+
// must not short-circuit this path. When validation is terminal, fall
|
|
403
|
+
// through to the default active-push below so `complete-milestone` can
|
|
404
|
+
// re-run idempotently.
|
|
405
|
+
if (!validationTerminal) {
|
|
395
406
|
activeMilestone = { id: m.id, title };
|
|
396
407
|
activeMilestoneSlices = slices;
|
|
397
408
|
activeMilestoneFound = true;
|
|
@@ -516,6 +527,9 @@ function resolveSliceDependencies(activeMilestoneSlices) {
|
|
|
516
527
|
return { activeSlice: null, activeSliceRow: null };
|
|
517
528
|
}
|
|
518
529
|
}
|
|
530
|
+
// First pass: find a slice with ALL dependencies satisfied (strict)
|
|
531
|
+
let bestFallback = null;
|
|
532
|
+
let bestFallbackSatisfied = -1;
|
|
519
533
|
for (const s of activeMilestoneSlices) {
|
|
520
534
|
if (isStatusDone(s.status))
|
|
521
535
|
continue;
|
|
@@ -524,6 +538,23 @@ function resolveSliceDependencies(activeMilestoneSlices) {
|
|
|
524
538
|
if (s.depends.every(dep => doneSliceIds.has(dep))) {
|
|
525
539
|
return { activeSlice: { id: s.id, title: s.title }, activeSliceRow: s };
|
|
526
540
|
}
|
|
541
|
+
// Track the slice with the most satisfied dependencies as fallback
|
|
542
|
+
const satisfied = s.depends.filter(dep => doneSliceIds.has(dep)).length;
|
|
543
|
+
if (satisfied > bestFallbackSatisfied || (satisfied === bestFallbackSatisfied && !bestFallback)) {
|
|
544
|
+
bestFallback = s;
|
|
545
|
+
bestFallbackSatisfied = satisfied;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Fallback: if no slice has all deps met but there ARE incomplete non-deferred
|
|
549
|
+
// slices, pick the one with the most deps satisfied. This prevents hard-blocking
|
|
550
|
+
// when dependency metadata is stale (e.g. after reassessment added/removed slices)
|
|
551
|
+
// or when deps reference slices from previous milestones.
|
|
552
|
+
if (bestFallback) {
|
|
553
|
+
const unmet = bestFallback.depends.filter(dep => !doneSliceIds.has(dep));
|
|
554
|
+
logWarning("state", `No slice has all deps satisfied — falling back to ${bestFallback.id} ` +
|
|
555
|
+
`(${bestFallbackSatisfied}/${bestFallback.depends.length} deps met, ` +
|
|
556
|
+
`unmet: ${unmet.join(", ")})`, { mid: activeMilestoneSlices[0]?.milestone_id, sid: bestFallback.id });
|
|
557
|
+
return { activeSlice: { id: bestFallback.id, title: bestFallback.title }, activeSliceRow: bestFallback };
|
|
527
558
|
}
|
|
528
559
|
return { activeSlice: null, activeSliceRow: null };
|
|
529
560
|
}
|
|
@@ -567,7 +598,7 @@ async function reconcileSliceTasks(basePath, milestoneId, sliceId, planFile) {
|
|
|
567
598
|
const summaryPath = resolveTaskFile(basePath, milestoneId, sliceId, t.id, "SUMMARY");
|
|
568
599
|
if (summaryPath && existsSync(summaryPath)) {
|
|
569
600
|
try {
|
|
570
|
-
updateTaskStatus(milestoneId, sliceId, t.id, "complete");
|
|
601
|
+
updateTaskStatus(milestoneId, sliceId, t.id, "complete", new Date().toISOString());
|
|
571
602
|
logWarning("reconcile", `task ${milestoneId}/${sliceId}/${t.id} status reconciled from "${t.status}" to "complete" (#2514)`, { mid: milestoneId, sid: sliceId, tid: t.id });
|
|
572
603
|
reconciled = true;
|
|
573
604
|
}
|
|
@@ -1269,6 +1300,8 @@ export async function _deriveStateImpl(basePath) {
|
|
|
1269
1300
|
}
|
|
1270
1301
|
}
|
|
1271
1302
|
else {
|
|
1303
|
+
let bestFallbackLegacy = null;
|
|
1304
|
+
let bestFallbackLegacySatisfied = -1;
|
|
1272
1305
|
for (const s of activeRoadmap.slices) {
|
|
1273
1306
|
if (s.done)
|
|
1274
1307
|
continue;
|
|
@@ -1276,6 +1309,20 @@ export async function _deriveStateImpl(basePath) {
|
|
|
1276
1309
|
activeSlice = { id: s.id, title: s.title };
|
|
1277
1310
|
break;
|
|
1278
1311
|
}
|
|
1312
|
+
// Track best fallback
|
|
1313
|
+
const satisfied = s.depends.filter(dep => doneSliceIds.has(dep)).length;
|
|
1314
|
+
if (satisfied > bestFallbackLegacySatisfied) {
|
|
1315
|
+
bestFallbackLegacy = s;
|
|
1316
|
+
bestFallbackLegacySatisfied = satisfied;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
// Fallback: if no slice has all deps met, pick the one with the most deps satisfied
|
|
1320
|
+
if (!activeSlice && bestFallbackLegacy) {
|
|
1321
|
+
const unmet = bestFallbackLegacy.depends.filter(dep => !doneSliceIds.has(dep));
|
|
1322
|
+
logWarning("state", `No slice has all deps satisfied — falling back to ${bestFallbackLegacy.id} ` +
|
|
1323
|
+
`(${bestFallbackLegacySatisfied}/${bestFallbackLegacy.depends.length} deps met, ` +
|
|
1324
|
+
`unmet: ${unmet.join(", ")})`);
|
|
1325
|
+
activeSlice = { id: bestFallbackLegacy.id, title: bestFallbackLegacy.title };
|
|
1279
1326
|
}
|
|
1280
1327
|
}
|
|
1281
1328
|
if (!activeSlice) {
|
package/dist/update-check.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare function compareSemver(a: string, b: string): number;
|
|
|
9
9
|
export declare function readUpdateCache(cachePath?: string): UpdateCheckCache | null;
|
|
10
10
|
export declare function writeUpdateCache(cache: UpdateCheckCache, cachePath?: string): void;
|
|
11
11
|
export declare function fetchLatestVersionFromRegistry(registryUrl?: string, fetchTimeoutMs?: number): Promise<string | null>;
|
|
12
|
+
export declare function resolveInstallCommand(pkg: string): string;
|
|
12
13
|
export interface UpdateCheckOptions {
|
|
13
14
|
currentVersion?: string;
|
|
14
15
|
cachePath?: string;
|