gsd-pi 2.73.1-dev.d987996 → 2.74.0-dev.2b524c3
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/claude-code-cli/stream-adapter.js +68 -4
- 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-model-selection.js +54 -11
- package/dist/resources/extensions/gsd/auto-post-unit.js +41 -1
- package/dist/resources/extensions/gsd/auto-start.js +23 -6
- 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/preferences-models.js +43 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +22 -0
- 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 +9 -9
- 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 +9 -9
- 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/index.d.ts +1 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- 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/index.ts +4 -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 +313 -8
- 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/assistant-message.d.ts +12 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +61 -28
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- 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 +94 -16
- 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/package.json +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +355 -8
- 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/assistant-message.ts +74 -32
- 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 +113 -21
- 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/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +107 -5
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +111 -2
- 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-model-selection.ts +85 -11
- package/src/resources/extensions/gsd/auto-post-unit.ts +47 -1
- package/src/resources/extensions/gsd/auto-start.ts +30 -6
- 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/preferences-models.ts +41 -0
- package/src/resources/extensions/gsd/preferences-types.ts +12 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +23 -0
- 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/auto-start-model-capture.test.ts +51 -2
- 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/flat-rate-routing-guard.test.ts +137 -1
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +91 -2
- package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -0
- 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/{cGmbVq2su4f9tMpgIkG8u → YzIEI9sxJy4t5xgClF08g}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{cGmbVq2su4f9tMpgIkG8u → YzIEI9sxJy4t5xgClF08g}/_ssgManifest.js +0 -0
|
@@ -83,7 +83,11 @@ import { join } from "node:path";
|
|
|
83
83
|
import { sep as pathSep } from "node:path";
|
|
84
84
|
|
|
85
85
|
import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
|
|
86
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
isCustomProvider,
|
|
88
|
+
resolveDefaultSessionModel,
|
|
89
|
+
resolveDynamicRoutingConfig,
|
|
90
|
+
} from "./preferences-models.js";
|
|
87
91
|
import type { WorktreeResolver } from "./worktree-resolver.js";
|
|
88
92
|
import { getSessionModelOverride } from "./session-model-override.js";
|
|
89
93
|
|
|
@@ -274,8 +278,18 @@ export async function bootstrapAutoSession(
|
|
|
274
278
|
//
|
|
275
279
|
// This preserves #3517 defaults while honoring explicit runtime model
|
|
276
280
|
// selection for subsequent /gsd runs in the same session.
|
|
281
|
+
//
|
|
282
|
+
// Exception (#4122): when the session provider is a custom provider declared
|
|
283
|
+
// in ~/.gsd/agent/models.json (Ollama, vLLM, OpenAI-compatible proxy, etc.),
|
|
284
|
+
// PREFERENCES.md is skipped entirely. PREFERENCES.md cannot reference custom
|
|
285
|
+
// providers, so honoring it would silently reroute auto-mode to a built-in
|
|
286
|
+
// provider the user is not logged into and surface as "Not logged in · Please
|
|
287
|
+
// run /login" before pausing and resetting to claude-code/claude-sonnet-4-6.
|
|
277
288
|
const manualSessionOverride = getSessionModelOverride(ctx.sessionManager.getSessionId());
|
|
278
|
-
const
|
|
289
|
+
const sessionProviderIsCustom = isCustomProvider(ctx.model?.provider);
|
|
290
|
+
const preferredModel = sessionProviderIsCustom
|
|
291
|
+
? null
|
|
292
|
+
: resolveDefaultSessionModel(ctx.model?.provider);
|
|
279
293
|
// Validate the preferred model against the live registry + provider auth so
|
|
280
294
|
// an unconfigured PREFERENCES.md entry (no API key / OAuth) can't become the
|
|
281
295
|
// start-model snapshot. Without this, every subsequent unit would try to
|
|
@@ -792,6 +806,9 @@ export async function bootstrapAutoSession(
|
|
|
792
806
|
|
|
793
807
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
794
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);
|
|
795
812
|
const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
|
|
796
813
|
const pendingCount = (state.registry ?? []).filter(
|
|
797
814
|
(m) => m.status !== "complete" && m.status !== "parked",
|
|
@@ -811,12 +828,19 @@ export async function bootstrapAutoSession(
|
|
|
811
828
|
? `${s.autoModeStartModel.provider}/${s.autoModeStartModel.id}`
|
|
812
829
|
: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "default";
|
|
813
830
|
|
|
814
|
-
// Flat-rate providers (e.g. GitHub Copilot, claude-code
|
|
815
|
-
//
|
|
816
|
-
|
|
831
|
+
// Flat-rate providers (e.g. GitHub Copilot, claude-code, user-declared
|
|
832
|
+
// subscription proxies, externalCli CLIs) suppress routing at dispatch
|
|
833
|
+
// time (#3453) — reflect that in the banner. Thread the same
|
|
834
|
+
// FlatRateContext used by selectAndApplyModel so user-declared
|
|
835
|
+
// flat-rate providers and externalCli auto-detection are respected.
|
|
836
|
+
const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
|
|
837
|
+
const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
|
|
817
838
|
const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
|
|
818
839
|
const effectivelyEnabled = routingConfig.enabled
|
|
819
|
-
&& !(effectiveProvider && isFlatRateProvider(
|
|
840
|
+
&& !(effectiveProvider && isFlatRateProvider(
|
|
841
|
+
effectiveProvider,
|
|
842
|
+
buildFlatRateContext(effectiveProvider, ctx, bannerPrefs),
|
|
843
|
+
));
|
|
820
844
|
|
|
821
845
|
// The actual ceiling may come from tier_models.heavy, not the start model.
|
|
822
846
|
const effectiveCeiling = (routingConfig.enabled && routingConfig.tier_models?.heavy)
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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(
|
|
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:
|
|
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: `
|
|
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
|
|
|
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 =
|
|
22
|
+
const REFRESH_INTERVAL_MS = 30_000;
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Initialize the always-on notification widget (belowEditor).
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
10
12
|
import type { DynamicRoutingConfig } from "./model-router.js";
|
|
11
13
|
import { defaultRoutingConfig } from "./model-router.js";
|
|
12
14
|
import type { TokenProfile, InlineLevel } from "./types.js";
|
|
@@ -185,6 +187,45 @@ export function resolveDefaultSessionModel(
|
|
|
185
187
|
return undefined;
|
|
186
188
|
}
|
|
187
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Returns true if `provider` is defined as a custom provider in the user's
|
|
192
|
+
* `~/.gsd/agent/models.json` (Ollama, vLLM, LM Studio, OpenAI-compatible
|
|
193
|
+
* proxies, etc.).
|
|
194
|
+
*
|
|
195
|
+
* Used by auto-mode bootstrap to decide whether the session model
|
|
196
|
+
* (set via `/gsd model`) should override `PREFERENCES.md`. Custom providers
|
|
197
|
+
* are never reachable from `PREFERENCES.md` (which only knows built-in
|
|
198
|
+
* providers), so when the user has explicitly selected one, it must take
|
|
199
|
+
* priority — otherwise auto-mode tries to start the built-in provider from
|
|
200
|
+
* PREFERENCES.md and fails with "Not logged in · Please run /login" (#4122).
|
|
201
|
+
*
|
|
202
|
+
* Reads models.json directly with a lightweight JSON parse to avoid
|
|
203
|
+
* pulling in the full model-registry at this call site. Falls back to
|
|
204
|
+
* `~/.pi/agent/models.json` for parity with `resolveModelsJsonPath()`.
|
|
205
|
+
* Any read or parse error yields `false` (treat as not-custom) so a
|
|
206
|
+
* malformed models.json never breaks the session bootstrap.
|
|
207
|
+
*/
|
|
208
|
+
export function isCustomProvider(provider: string | undefined): boolean {
|
|
209
|
+
if (!provider) return false;
|
|
210
|
+
const candidates = [
|
|
211
|
+
join(homedir(), ".gsd", "agent", "models.json"),
|
|
212
|
+
join(homedir(), ".pi", "agent", "models.json"),
|
|
213
|
+
];
|
|
214
|
+
for (const path of candidates) {
|
|
215
|
+
if (!existsSync(path)) continue;
|
|
216
|
+
try {
|
|
217
|
+
const raw = readFileSync(path, "utf-8");
|
|
218
|
+
const parsed = JSON.parse(raw) as { providers?: Record<string, unknown> };
|
|
219
|
+
if (parsed?.providers && Object.prototype.hasOwnProperty.call(parsed.providers, provider)) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Ignore — malformed models.json must not break bootstrap.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
188
229
|
/**
|
|
189
230
|
* Determines the next fallback model to try when the current model fails.
|
|
190
231
|
* If the current model is not in the configured list, returns the primary model.
|
|
@@ -113,6 +113,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
113
113
|
"discuss_preparation",
|
|
114
114
|
"discuss_web_research",
|
|
115
115
|
"discuss_depth",
|
|
116
|
+
"flat_rate_providers",
|
|
116
117
|
]);
|
|
117
118
|
|
|
118
119
|
/** Canonical list of all dispatch unit types. */
|
|
@@ -359,6 +360,17 @@ export interface GSDPreferences {
|
|
|
359
360
|
* Default: "standard".
|
|
360
361
|
*/
|
|
361
362
|
discuss_depth?: "quick" | "standard" | "thorough";
|
|
363
|
+
/**
|
|
364
|
+
* Extra provider IDs to treat as flat-rate (no cost benefit from dynamic
|
|
365
|
+
* routing). Dynamic routing is suppressed for any provider listed here,
|
|
366
|
+
* in addition to the built-in list (github-copilot, copilot, claude-code)
|
|
367
|
+
* and any provider auto-detected via `authMode: "externalCli"`.
|
|
368
|
+
*
|
|
369
|
+
* Intended for private subscription-backed proxies, enterprise-gated
|
|
370
|
+
* deployments, and custom CLI wrappers where every request costs the
|
|
371
|
+
* same regardless of model. Case-insensitive.
|
|
372
|
+
*/
|
|
373
|
+
flat_rate_providers?: string[];
|
|
362
374
|
}
|
|
363
375
|
|
|
364
376
|
export interface LoadedGSDPreferences {
|
|
@@ -180,6 +180,29 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// ─── Flat-rate Providers ────────────────────────────────────────────
|
|
184
|
+
// User-declared flat-rate providers for dynamic routing suppression.
|
|
185
|
+
// Built-in providers (github-copilot, copilot, claude-code) and any
|
|
186
|
+
// externalCli provider are already auto-detected; this list layers on
|
|
187
|
+
// top for private subscription proxies and custom CLI wrappers.
|
|
188
|
+
if (preferences.flat_rate_providers !== undefined) {
|
|
189
|
+
if (Array.isArray(preferences.flat_rate_providers)) {
|
|
190
|
+
const allStrings = preferences.flat_rate_providers.every(
|
|
191
|
+
(item: unknown) => typeof item === "string",
|
|
192
|
+
);
|
|
193
|
+
if (allStrings) {
|
|
194
|
+
// Strip empty/whitespace-only entries to avoid false matches.
|
|
195
|
+
validated.flat_rate_providers = preferences.flat_rate_providers
|
|
196
|
+
.map((s: string) => s.trim())
|
|
197
|
+
.filter((s: string) => s.length > 0);
|
|
198
|
+
} else {
|
|
199
|
+
errors.push("flat_rate_providers must be an array of strings");
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
errors.push("flat_rate_providers must be an array of strings");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
183
206
|
// ─── Phase Skip Preferences ─────────────────────────────────────────
|
|
184
207
|
if (preferences.phases !== undefined) {
|
|
185
208
|
if (typeof preferences.phases === "object" && preferences.phases !== null) {
|
|
@@ -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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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 (
|
|
437
|
-
const
|
|
438
|
-
if (
|
|
439
|
-
|
|
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
|
-
|
|
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("
|
|
692
|
-
"should have called
|
|
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"),
|