gsd-pi 2.73.0-dev.e1c09f2 → 2.73.1-dev.06e4302
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 +34 -9
- package/dist/resources/extensions/gsd/bootstrap/crash-log.js +31 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +18 -7
- package/dist/resources/extensions/gsd/commands-handlers.js +8 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +51 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +36 -2
- package/dist/resources/extensions/gsd/milestone-actions.js +19 -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 +12 -12
- 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 +12 -12
- 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 +36 -14
- package/src/resources/extensions/gsd/bootstrap/crash-log.ts +32 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +19 -7
- package/src/resources/extensions/gsd/commands-handlers.ts +8 -2
- package/src/resources/extensions/gsd/crash-recovery.ts +59 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/src/resources/extensions/gsd/gsd-db.ts +52 -2
- package/src/resources/extensions/gsd/milestone-actions.ts +19 -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/crash-handler-secondary.test.ts +235 -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/gsd-db.test.ts +59 -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/park-milestone.test.ts +64 -0
- 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/{_XD_gUDcZNBbWV5rI8RgS → RXD20AQgB9BHSQJ07MDdd}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → RXD20AQgB9BHSQJ07MDdd}/_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 */ }
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* AssistantMessageEvents for TUI rendering, then strips tool-call blocks from
|
|
7
7
|
* the final AssistantMessage so GSD's agent loop doesn't try to dispatch them.
|
|
8
8
|
*/
|
|
9
|
-
import { EventStream } from "@gsd/pi-ai";
|
|
9
|
+
import { EventStream, mapThinkingLevelToEffort, supportsAdaptiveThinking } from "@gsd/pi-ai";
|
|
10
10
|
import { execSync } from "node:child_process";
|
|
11
11
|
import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js";
|
|
12
12
|
import { buildWorkflowMcpServers } from "../gsd/workflow-mcp.js";
|
|
@@ -123,6 +123,63 @@ export function buildPromptFromContext(context) {
|
|
|
123
123
|
}
|
|
124
124
|
return parts.join("\n\n");
|
|
125
125
|
}
|
|
126
|
+
function stripDataUriPrefix(value) {
|
|
127
|
+
const commaIndex = value.indexOf(",");
|
|
128
|
+
if (value.startsWith("data:") && commaIndex !== -1) {
|
|
129
|
+
return value.slice(commaIndex + 1);
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
function inferMimeTypeFromDataUri(value) {
|
|
134
|
+
const match = /^data:([^;,]+);base64,/.exec(value);
|
|
135
|
+
return match?.[1] ?? null;
|
|
136
|
+
}
|
|
137
|
+
export function extractImageBlocksFromContext(context) {
|
|
138
|
+
const imageBlocks = [];
|
|
139
|
+
for (const msg of context.messages) {
|
|
140
|
+
if (msg.role !== "user" || !Array.isArray(msg.content))
|
|
141
|
+
continue;
|
|
142
|
+
for (const part of msg.content) {
|
|
143
|
+
if (!part || typeof part !== "object")
|
|
144
|
+
continue;
|
|
145
|
+
const block = part;
|
|
146
|
+
if (block.type !== "image" || typeof block.data !== "string")
|
|
147
|
+
continue;
|
|
148
|
+
const mimeType = typeof block.mimeType === "string" && block.mimeType.length > 0
|
|
149
|
+
? block.mimeType
|
|
150
|
+
: inferMimeTypeFromDataUri(block.data);
|
|
151
|
+
if (!mimeType)
|
|
152
|
+
continue;
|
|
153
|
+
imageBlocks.push({
|
|
154
|
+
type: "image",
|
|
155
|
+
source: {
|
|
156
|
+
type: "base64",
|
|
157
|
+
media_type: mimeType,
|
|
158
|
+
data: stripDataUriPrefix(block.data),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return imageBlocks;
|
|
164
|
+
}
|
|
165
|
+
export function buildSdkQueryPrompt(context, textPrompt = buildPromptFromContext(context)) {
|
|
166
|
+
const imageBlocks = extractImageBlocksFromContext(context);
|
|
167
|
+
if (imageBlocks.length === 0) {
|
|
168
|
+
return textPrompt;
|
|
169
|
+
}
|
|
170
|
+
const content = [...imageBlocks];
|
|
171
|
+
if (textPrompt) {
|
|
172
|
+
content.push({ type: "text", text: textPrompt });
|
|
173
|
+
}
|
|
174
|
+
const sdkMessage = {
|
|
175
|
+
type: "user",
|
|
176
|
+
message: { role: "user", content },
|
|
177
|
+
parent_tool_use_id: null,
|
|
178
|
+
};
|
|
179
|
+
return (async function* () {
|
|
180
|
+
yield sdkMessage;
|
|
181
|
+
})();
|
|
182
|
+
}
|
|
126
183
|
// ---------------------------------------------------------------------------
|
|
127
184
|
// Error helper
|
|
128
185
|
// ---------------------------------------------------------------------------
|
|
@@ -437,6 +494,7 @@ export async function resolveClaudePermissionMode(env = process.env) {
|
|
|
437
494
|
* behaviour pass `permissionMode: "bypassPermissions"` explicitly.
|
|
438
495
|
*/
|
|
439
496
|
export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
|
|
497
|
+
const { reasoning, ...sdkExtraOptions } = extraOptions;
|
|
440
498
|
const mcpServers = buildWorkflowMcpServers();
|
|
441
499
|
const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
|
|
442
500
|
const disallowedTools = ["AskUserQuestion"];
|
|
@@ -455,6 +513,9 @@ export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
|
|
|
455
513
|
"Bash(pwd)",
|
|
456
514
|
...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
|
|
457
515
|
];
|
|
516
|
+
const effort = reasoning && supportsAdaptiveThinking(modelId)
|
|
517
|
+
? mapThinkingLevelToEffort(reasoning, modelId)
|
|
518
|
+
: undefined;
|
|
458
519
|
return {
|
|
459
520
|
pathToClaudeCodeExecutable: getClaudePath(),
|
|
460
521
|
model: modelId,
|
|
@@ -469,7 +530,8 @@ export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
|
|
|
469
530
|
...(allowedTools.length > 0 ? { allowedTools } : {}),
|
|
470
531
|
...(mcpServers ? { mcpServers } : {}),
|
|
471
532
|
betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
|
|
472
|
-
...
|
|
533
|
+
...(effort ? { effort } : {}),
|
|
534
|
+
...sdkExtraOptions,
|
|
473
535
|
};
|
|
474
536
|
}
|
|
475
537
|
function normalizeToolResultContent(content) {
|
|
@@ -617,14 +679,16 @@ async function pumpSdkMessages(model, context, options, stream) {
|
|
|
617
679
|
options.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
618
680
|
}
|
|
619
681
|
const prompt = buildPromptFromContext(context);
|
|
682
|
+
const queryPrompt = buildSdkQueryPrompt(context, prompt);
|
|
620
683
|
const permissionMode = await resolveClaudePermissionMode();
|
|
621
684
|
const sdkOpts = buildSdkOptions(modelId, prompt, { permissionMode }, typeof options?.extensionUIContext === "object"
|
|
622
685
|
? {
|
|
686
|
+
reasoning: options?.reasoning,
|
|
623
687
|
onElicitation: createClaudeCodeElicitationHandler(options?.extensionUIContext),
|
|
624
688
|
}
|
|
625
|
-
: {});
|
|
689
|
+
: { reasoning: options?.reasoning });
|
|
626
690
|
const queryResult = sdk.query({
|
|
627
|
-
prompt,
|
|
691
|
+
prompt: queryPrompt,
|
|
628
692
|
options: {
|
|
629
693
|
...sdkOpts,
|
|
630
694
|
abortController: controller,
|
|
@@ -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
|
}
|
|
@@ -9,10 +9,8 @@ import { resolveModelForComplexity, escalateTier, getEligibleModels, loadCapabil
|
|
|
9
9
|
import { getLedger, getProjectTotals } from "./metrics.js";
|
|
10
10
|
import { unitPhaseLabel } from "./auto-dashboard.js";
|
|
11
11
|
import { getSessionModelOverride } from "./session-model-override.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* synthesize a routing ceiling from dynamic_routing.tier_models (#3962). */
|
|
15
|
-
isAutoMode = true) {
|
|
12
|
+
import { logWarning } from "./workflow-logger.js";
|
|
13
|
+
export function resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode = true) {
|
|
16
14
|
const explicitConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
17
15
|
if (explicitConfig)
|
|
18
16
|
return explicitConfig;
|
|
@@ -24,7 +22,7 @@ isAutoMode = true) {
|
|
|
24
22
|
if (!routingConfig.enabled || !routingConfig.tier_models)
|
|
25
23
|
return undefined;
|
|
26
24
|
// Don't synthesize a routing config for flat-rate providers (#3453).
|
|
27
|
-
if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
|
|
25
|
+
if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx))
|
|
28
26
|
return undefined;
|
|
29
27
|
const ceilingModel = routingConfig.tier_models.heavy
|
|
30
28
|
?? (autoModeStartModel ? `${autoModeStartModel.provider}/${autoModeStartModel.id}` : undefined);
|
|
@@ -51,6 +49,17 @@ sessionModelOverride) {
|
|
|
51
49
|
const effectiveSessionModelOverride = sessionModelOverride === undefined
|
|
52
50
|
? getSessionModelOverride(ctx.sessionManager.getSessionId())
|
|
53
51
|
: (sessionModelOverride ?? undefined);
|
|
52
|
+
// Enrich the start model with a flat-rate context up front so routing
|
|
53
|
+
// synthesis and the dispatch-time guard see the same signals (built-in
|
|
54
|
+
// list + user `flat_rate_providers` preference + externalCli auto-
|
|
55
|
+
// detection). The dispatch-time primary-model check below builds its
|
|
56
|
+
// own per-provider context when it has a resolved primary model.
|
|
57
|
+
if (autoModeStartModel) {
|
|
58
|
+
autoModeStartModel = {
|
|
59
|
+
...autoModeStartModel,
|
|
60
|
+
flatRateCtx: buildFlatRateContext(autoModeStartModel.provider, ctx, prefs),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
54
63
|
const modelConfig = effectiveSessionModelOverride
|
|
55
64
|
? undefined
|
|
56
65
|
: resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode);
|
|
@@ -76,12 +85,13 @@ sessionModelOverride) {
|
|
|
76
85
|
if (routingConfig.enabled) {
|
|
77
86
|
const primaryModel = resolveModelId(modelConfig.primary, availableModels, ctx.model?.provider);
|
|
78
87
|
if (primaryModel) {
|
|
79
|
-
|
|
88
|
+
const primaryFlatRateCtx = buildFlatRateContext(primaryModel.provider, ctx, prefs);
|
|
89
|
+
if (isFlatRateProvider(primaryModel.provider, primaryFlatRateCtx)) {
|
|
80
90
|
routingConfig.enabled = false;
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
|
-
else if ((autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
|
|
84
|
-
|| (ctx.model?.provider && isFlatRateProvider(ctx.model.provider))) {
|
|
93
|
+
else if ((autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx))
|
|
94
|
+
|| (ctx.model?.provider && isFlatRateProvider(ctx.model.provider, buildFlatRateContext(ctx.model.provider, ctx, prefs)))) {
|
|
85
95
|
// Primary model unresolvable but provider signals indicate flat-rate —
|
|
86
96
|
// disable routing to prevent quality degradation.
|
|
87
97
|
routingConfig.enabled = false;
|
|
@@ -331,7 +341,40 @@ export function resolveModelId(modelId, availableModels, currentProvider) {
|
|
|
331
341
|
* Uses case-insensitive matching with alias support to prevent fail-open on
|
|
332
342
|
* provider naming variations (e.g. "copilot" vs "github-copilot").
|
|
333
343
|
*/
|
|
334
|
-
const
|
|
335
|
-
export function isFlatRateProvider(provider) {
|
|
336
|
-
|
|
344
|
+
const BUILTIN_FLAT_RATE = new Set(["github-copilot", "copilot", "claude-code"]);
|
|
345
|
+
export function isFlatRateProvider(provider, opts) {
|
|
346
|
+
const p = provider.toLowerCase();
|
|
347
|
+
if (BUILTIN_FLAT_RATE.has(p))
|
|
348
|
+
return true;
|
|
349
|
+
if (opts?.userFlatRate?.some(id => id.toLowerCase() === p))
|
|
350
|
+
return true;
|
|
351
|
+
if (opts?.authMode === "externalCli")
|
|
352
|
+
return true;
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Build a FlatRateContext for a given provider from live runtime state.
|
|
357
|
+
* Safe to call when ctx or prefs are undefined — missing pieces are
|
|
358
|
+
* treated as "no signal".
|
|
359
|
+
*/
|
|
360
|
+
export function buildFlatRateContext(provider, ctx, prefs) {
|
|
361
|
+
let authMode;
|
|
362
|
+
const getAuthMode = ctx?.modelRegistry?.getProviderAuthMode;
|
|
363
|
+
if (typeof getAuthMode === "function") {
|
|
364
|
+
try {
|
|
365
|
+
const mode = getAuthMode(provider);
|
|
366
|
+
if (mode === "apiKey" || mode === "oauth" || mode === "externalCli" || mode === "none") {
|
|
367
|
+
authMode = mode;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
// Registry lookup failure must never break flat-rate detection —
|
|
372
|
+
// fall through with authMode undefined and surface the cause.
|
|
373
|
+
logWarning("dispatch", `flat-rate auth-mode lookup failed for ${provider}: ${err instanceof Error ? err.message : String(err)}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
authMode,
|
|
378
|
+
userFlatRate: prefs?.flat_rate_providers,
|
|
379
|
+
};
|
|
337
380
|
}
|
|
@@ -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";
|
|
@@ -38,7 +38,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync, } fro
|
|
|
38
38
|
import { join } from "node:path";
|
|
39
39
|
import { sep as pathSep } from "node:path";
|
|
40
40
|
import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
|
|
41
|
-
import { resolveDefaultSessionModel, resolveDynamicRoutingConfig } from "./preferences-models.js";
|
|
41
|
+
import { isCustomProvider, resolveDefaultSessionModel, resolveDynamicRoutingConfig, } from "./preferences-models.js";
|
|
42
42
|
import { getSessionModelOverride } from "./session-model-override.js";
|
|
43
43
|
/**
|
|
44
44
|
* Bootstrap a fresh auto-mode session. Handles everything from git init
|
|
@@ -195,8 +195,18 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
195
195
|
//
|
|
196
196
|
// This preserves #3517 defaults while honoring explicit runtime model
|
|
197
197
|
// selection for subsequent /gsd runs in the same session.
|
|
198
|
+
//
|
|
199
|
+
// Exception (#4122): when the session provider is a custom provider declared
|
|
200
|
+
// in ~/.gsd/agent/models.json (Ollama, vLLM, OpenAI-compatible proxy, etc.),
|
|
201
|
+
// PREFERENCES.md is skipped entirely. PREFERENCES.md cannot reference custom
|
|
202
|
+
// providers, so honoring it would silently reroute auto-mode to a built-in
|
|
203
|
+
// provider the user is not logged into and surface as "Not logged in · Please
|
|
204
|
+
// run /login" before pausing and resetting to claude-code/claude-sonnet-4-6.
|
|
198
205
|
const manualSessionOverride = getSessionModelOverride(ctx.sessionManager.getSessionId());
|
|
199
|
-
const
|
|
206
|
+
const sessionProviderIsCustom = isCustomProvider(ctx.model?.provider);
|
|
207
|
+
const preferredModel = sessionProviderIsCustom
|
|
208
|
+
? null
|
|
209
|
+
: resolveDefaultSessionModel(ctx.model?.provider);
|
|
200
210
|
// Validate the preferred model against the live registry + provider auth so
|
|
201
211
|
// an unconfigured PREFERENCES.md entry (no API key / OAuth) can't become the
|
|
202
212
|
// start-model snapshot. Without this, every subsequent unit would try to
|
|
@@ -622,6 +632,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
622
632
|
}
|
|
623
633
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
624
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);
|
|
625
638
|
const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
|
|
626
639
|
const pendingCount = (state.registry ?? []).filter((m) => m.status !== "complete" && m.status !== "parked").length;
|
|
627
640
|
const scopeMsg = pendingCount > 1
|
|
@@ -636,12 +649,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
636
649
|
const startModelLabel = s.autoModeStartModel
|
|
637
650
|
? `${s.autoModeStartModel.provider}/${s.autoModeStartModel.id}`
|
|
638
651
|
: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "default";
|
|
639
|
-
// Flat-rate providers (e.g. GitHub Copilot, claude-code
|
|
640
|
-
//
|
|
641
|
-
|
|
652
|
+
// Flat-rate providers (e.g. GitHub Copilot, claude-code, user-declared
|
|
653
|
+
// subscription proxies, externalCli CLIs) suppress routing at dispatch
|
|
654
|
+
// time (#3453) — reflect that in the banner. Thread the same
|
|
655
|
+
// FlatRateContext used by selectAndApplyModel so user-declared
|
|
656
|
+
// flat-rate providers and externalCli auto-detection are respected.
|
|
657
|
+
const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
|
|
658
|
+
const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
|
|
642
659
|
const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
|
|
643
660
|
const effectivelyEnabled = routingConfig.enabled
|
|
644
|
-
&& !(effectiveProvider && isFlatRateProvider(effectiveProvider));
|
|
661
|
+
&& !(effectiveProvider && isFlatRateProvider(effectiveProvider, buildFlatRateContext(effectiveProvider, ctx, bannerPrefs)));
|
|
645
662
|
// The actual ceiling may come from tier_models.heavy, not the start model.
|
|
646
663
|
const effectiveCeiling = (routingConfig.enabled && routingConfig.tier_models?.heavy)
|
|
647
664
|
? routingConfig.tier_models.heavy
|
|
@@ -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 {
|