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
|
@@ -14,10 +14,11 @@ import type {
|
|
|
14
14
|
Context,
|
|
15
15
|
Model,
|
|
16
16
|
SimpleStreamOptions,
|
|
17
|
+
ThinkingLevel,
|
|
17
18
|
ToolCall,
|
|
18
19
|
} from "@gsd/pi-ai";
|
|
19
20
|
import type { ExtensionUIContext } from "@gsd/pi-coding-agent";
|
|
20
|
-
import { EventStream } from "@gsd/pi-ai";
|
|
21
|
+
import { EventStream, mapThinkingLevelToEffort, supportsAdaptiveThinking } from "@gsd/pi-ai";
|
|
21
22
|
import { execSync } from "node:child_process";
|
|
22
23
|
import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js";
|
|
23
24
|
import { buildWorkflowMcpServers } from "../gsd/workflow-mcp.js";
|
|
@@ -96,6 +97,31 @@ interface ParsedTextInputField {
|
|
|
96
97
|
secure: boolean;
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
interface SDKInputImageBlock {
|
|
101
|
+
type: "image";
|
|
102
|
+
source: {
|
|
103
|
+
type: "base64";
|
|
104
|
+
media_type: string;
|
|
105
|
+
data: string;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface SDKInputTextBlock {
|
|
110
|
+
type: "text";
|
|
111
|
+
text: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type SDKInputUserContentBlock = SDKInputImageBlock | SDKInputTextBlock;
|
|
115
|
+
|
|
116
|
+
interface SDKInputUserMessage {
|
|
117
|
+
type: "user";
|
|
118
|
+
message: {
|
|
119
|
+
role: "user";
|
|
120
|
+
content: SDKInputUserContentBlock[];
|
|
121
|
+
};
|
|
122
|
+
parent_tool_use_id: null;
|
|
123
|
+
}
|
|
124
|
+
|
|
99
125
|
const OTHER_OPTION_LABEL = "None of the above";
|
|
100
126
|
const SENSITIVE_FIELD_PATTERN = /(password|passphrase|secret|token|api[_\s-]*key|private[_\s-]*key|credential)/i;
|
|
101
127
|
|
|
@@ -222,6 +248,74 @@ export function buildPromptFromContext(context: Context): string {
|
|
|
222
248
|
return parts.join("\n\n");
|
|
223
249
|
}
|
|
224
250
|
|
|
251
|
+
function stripDataUriPrefix(value: string): string {
|
|
252
|
+
const commaIndex = value.indexOf(",");
|
|
253
|
+
if (value.startsWith("data:") && commaIndex !== -1) {
|
|
254
|
+
return value.slice(commaIndex + 1);
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function inferMimeTypeFromDataUri(value: string): string | null {
|
|
260
|
+
const match = /^data:([^;,]+);base64,/.exec(value);
|
|
261
|
+
return match?.[1] ?? null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function extractImageBlocksFromContext(context: Context): SDKInputImageBlock[] {
|
|
265
|
+
const imageBlocks: SDKInputImageBlock[] = [];
|
|
266
|
+
|
|
267
|
+
for (const msg of context.messages) {
|
|
268
|
+
if (msg.role !== "user" || !Array.isArray(msg.content)) continue;
|
|
269
|
+
for (const part of msg.content) {
|
|
270
|
+
if (!part || typeof part !== "object") continue;
|
|
271
|
+
const block = part as { type?: unknown; data?: unknown; mimeType?: unknown };
|
|
272
|
+
if (block.type !== "image" || typeof block.data !== "string") continue;
|
|
273
|
+
|
|
274
|
+
const mimeType =
|
|
275
|
+
typeof block.mimeType === "string" && block.mimeType.length > 0
|
|
276
|
+
? block.mimeType
|
|
277
|
+
: inferMimeTypeFromDataUri(block.data);
|
|
278
|
+
if (!mimeType) continue;
|
|
279
|
+
|
|
280
|
+
imageBlocks.push({
|
|
281
|
+
type: "image",
|
|
282
|
+
source: {
|
|
283
|
+
type: "base64",
|
|
284
|
+
media_type: mimeType,
|
|
285
|
+
data: stripDataUriPrefix(block.data),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return imageBlocks;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function buildSdkQueryPrompt(
|
|
295
|
+
context: Context,
|
|
296
|
+
textPrompt: string = buildPromptFromContext(context),
|
|
297
|
+
): string | AsyncIterable<SDKInputUserMessage> {
|
|
298
|
+
const imageBlocks = extractImageBlocksFromContext(context);
|
|
299
|
+
if (imageBlocks.length === 0) {
|
|
300
|
+
return textPrompt;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const content: SDKInputUserContentBlock[] = [...imageBlocks];
|
|
304
|
+
if (textPrompt) {
|
|
305
|
+
content.push({ type: "text", text: textPrompt });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const sdkMessage: SDKInputUserMessage = {
|
|
309
|
+
type: "user",
|
|
310
|
+
message: { role: "user", content },
|
|
311
|
+
parent_tool_use_id: null,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return (async function* () {
|
|
315
|
+
yield sdkMessage;
|
|
316
|
+
})();
|
|
317
|
+
}
|
|
318
|
+
|
|
225
319
|
// ---------------------------------------------------------------------------
|
|
226
320
|
// Error helper
|
|
227
321
|
// ---------------------------------------------------------------------------
|
|
@@ -600,8 +694,9 @@ export function buildSdkOptions(
|
|
|
600
694
|
modelId: string,
|
|
601
695
|
prompt: string,
|
|
602
696
|
overrides?: { permissionMode?: "bypassPermissions" | "acceptEdits" | "default" | "plan" },
|
|
603
|
-
extraOptions: Record<string, unknown> = {},
|
|
697
|
+
extraOptions: Record<string, unknown> & { reasoning?: ThinkingLevel } = {},
|
|
604
698
|
): Record<string, unknown> {
|
|
699
|
+
const { reasoning, ...sdkExtraOptions } = extraOptions;
|
|
605
700
|
const mcpServers = buildWorkflowMcpServers();
|
|
606
701
|
const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
|
|
607
702
|
const disallowedTools = ["AskUserQuestion"];
|
|
@@ -620,6 +715,10 @@ export function buildSdkOptions(
|
|
|
620
715
|
"Bash(pwd)",
|
|
621
716
|
...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
|
|
622
717
|
];
|
|
718
|
+
const effort =
|
|
719
|
+
reasoning && supportsAdaptiveThinking(modelId)
|
|
720
|
+
? mapThinkingLevelToEffort(reasoning, modelId)
|
|
721
|
+
: undefined;
|
|
623
722
|
return {
|
|
624
723
|
pathToClaudeCodeExecutable: getClaudePath(),
|
|
625
724
|
model: modelId,
|
|
@@ -634,7 +733,8 @@ export function buildSdkOptions(
|
|
|
634
733
|
...(allowedTools.length > 0 ? { allowedTools } : {}),
|
|
635
734
|
...(mcpServers ? { mcpServers } : {}),
|
|
636
735
|
betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
|
|
637
|
-
...
|
|
736
|
+
...(effort ? { effort } : {}),
|
|
737
|
+
...sdkExtraOptions,
|
|
638
738
|
};
|
|
639
739
|
}
|
|
640
740
|
|
|
@@ -821,6 +921,7 @@ async function pumpSdkMessages(
|
|
|
821
921
|
}
|
|
822
922
|
|
|
823
923
|
const prompt = buildPromptFromContext(context);
|
|
924
|
+
const queryPrompt = buildSdkQueryPrompt(context, prompt);
|
|
824
925
|
const permissionMode = await resolveClaudePermissionMode();
|
|
825
926
|
const sdkOpts = buildSdkOptions(
|
|
826
927
|
modelId,
|
|
@@ -828,15 +929,16 @@ async function pumpSdkMessages(
|
|
|
828
929
|
{ permissionMode },
|
|
829
930
|
typeof (options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext === "object"
|
|
830
931
|
? {
|
|
932
|
+
reasoning: options?.reasoning,
|
|
831
933
|
onElicitation: createClaudeCodeElicitationHandler(
|
|
832
934
|
(options as ClaudeCodeStreamOptions | undefined)?.extensionUIContext,
|
|
833
935
|
),
|
|
834
936
|
}
|
|
835
|
-
: {},
|
|
937
|
+
: { reasoning: options?.reasoning },
|
|
836
938
|
);
|
|
837
939
|
|
|
838
940
|
const queryResult = sdk.query({
|
|
839
|
-
prompt,
|
|
941
|
+
prompt: queryPrompt,
|
|
840
942
|
options: {
|
|
841
943
|
...sdkOpts,
|
|
842
944
|
abortController: controller,
|
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
mergePendingToolCalls,
|
|
11
11
|
resolveClaudePermissionMode,
|
|
12
12
|
buildPromptFromContext,
|
|
13
|
+
buildSdkQueryPrompt,
|
|
13
14
|
buildSdkOptions,
|
|
14
15
|
createClaudeCodeElicitationHandler,
|
|
16
|
+
extractImageBlocksFromContext,
|
|
15
17
|
extractToolResultsFromSdkUserMessage,
|
|
16
18
|
getClaudeLookupCommand,
|
|
17
19
|
parseAskUserQuestionsElicitation,
|
|
@@ -167,6 +169,92 @@ describe("stream-adapter — full context prompt (#2859)", () => {
|
|
|
167
169
|
});
|
|
168
170
|
});
|
|
169
171
|
|
|
172
|
+
describe("stream-adapter — image prompt forwarding (#4183)", () => {
|
|
173
|
+
test("extractImageBlocksFromContext maps user image parts to Anthropic base64 image blocks", () => {
|
|
174
|
+
const context: Context = {
|
|
175
|
+
messages: [
|
|
176
|
+
{
|
|
177
|
+
role: "user",
|
|
178
|
+
content: [
|
|
179
|
+
{ type: "text", text: "look" },
|
|
180
|
+
{
|
|
181
|
+
type: "image",
|
|
182
|
+
data: "data:image/png;base64,abc123",
|
|
183
|
+
mimeType: "image/png",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
} as Message,
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const imageBlocks = extractImageBlocksFromContext(context);
|
|
191
|
+
assert.deepEqual(imageBlocks, [
|
|
192
|
+
{
|
|
193
|
+
type: "image",
|
|
194
|
+
source: {
|
|
195
|
+
type: "base64",
|
|
196
|
+
media_type: "image/png",
|
|
197
|
+
data: "abc123",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("buildSdkQueryPrompt returns plain string when no images exist in context", () => {
|
|
204
|
+
const context: Context = {
|
|
205
|
+
messages: [{ role: "user", content: "hello" } as Message],
|
|
206
|
+
};
|
|
207
|
+
const textPrompt = buildPromptFromContext(context);
|
|
208
|
+
|
|
209
|
+
const prompt = buildSdkQueryPrompt(context, textPrompt);
|
|
210
|
+
assert.equal(typeof prompt, "string");
|
|
211
|
+
assert.equal(prompt, textPrompt);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("buildSdkQueryPrompt wraps images and prompt text in an SDK user message iterable", async () => {
|
|
215
|
+
const context: Context = {
|
|
216
|
+
messages: [
|
|
217
|
+
{
|
|
218
|
+
role: "user",
|
|
219
|
+
content: [
|
|
220
|
+
{ type: "image", data: "ZmFrZQ==", mimeType: "image/jpeg" },
|
|
221
|
+
{ type: "text", text: "What is in this image?" },
|
|
222
|
+
],
|
|
223
|
+
} as Message,
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
const textPrompt = buildPromptFromContext(context);
|
|
227
|
+
|
|
228
|
+
const prompt = buildSdkQueryPrompt(context, textPrompt);
|
|
229
|
+
assert.notEqual(typeof prompt, "string");
|
|
230
|
+
assert.ok(prompt && typeof (prompt as any)[Symbol.asyncIterator] === "function");
|
|
231
|
+
|
|
232
|
+
const messages: any[] = [];
|
|
233
|
+
for await (const item of prompt as AsyncIterable<any>) {
|
|
234
|
+
messages.push(item);
|
|
235
|
+
}
|
|
236
|
+
assert.equal(messages.length, 1);
|
|
237
|
+
assert.deepEqual(messages[0], {
|
|
238
|
+
type: "user",
|
|
239
|
+
message: {
|
|
240
|
+
role: "user",
|
|
241
|
+
content: [
|
|
242
|
+
{
|
|
243
|
+
type: "image",
|
|
244
|
+
source: {
|
|
245
|
+
type: "base64",
|
|
246
|
+
media_type: "image/jpeg",
|
|
247
|
+
data: "ZmFrZQ==",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{ type: "text", text: textPrompt },
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
parent_tool_use_id: null,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
170
258
|
// ---------------------------------------------------------------------------
|
|
171
259
|
// Bug #4102 — transcript fabrication regression tests
|
|
172
260
|
// ---------------------------------------------------------------------------
|
|
@@ -343,6 +431,26 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
343
431
|
);
|
|
344
432
|
});
|
|
345
433
|
|
|
434
|
+
test("buildSdkOptions maps reasoning to effort for adaptive Claude Code models (#3917)", () => {
|
|
435
|
+
const options = buildSdkOptions("claude-sonnet-4-6", "test", undefined, { reasoning: "high" });
|
|
436
|
+
assert.equal(options.effort, "high");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("buildSdkOptions upgrades xhigh reasoning to max for opus 4.6 (#3917)", () => {
|
|
440
|
+
const options = buildSdkOptions("claude-opus-4-6", "test", undefined, { reasoning: "xhigh" });
|
|
441
|
+
assert.equal(options.effort, "max");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("buildSdkOptions omits effort when reasoning is undefined (#3917)", () => {
|
|
445
|
+
const options = buildSdkOptions("claude-sonnet-4-6", "test");
|
|
446
|
+
assert.equal("effort" in options, false);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("buildSdkOptions omits effort for non-adaptive Claude models (#3917)", () => {
|
|
450
|
+
const options = buildSdkOptions("claude-sonnet-4-20250514", "test", undefined, { reasoning: "high" });
|
|
451
|
+
assert.equal("effort" in options, false);
|
|
452
|
+
});
|
|
453
|
+
|
|
346
454
|
test("buildSdkOptions includes workflow MCP server config when env is set", () => {
|
|
347
455
|
const prev = {
|
|
348
456
|
GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
|
|
@@ -774,11 +882,12 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
774
882
|
},
|
|
775
883
|
};
|
|
776
884
|
|
|
885
|
+
const secureValue = "ui-collected-value";
|
|
777
886
|
const inputCalls: Array<{ opts?: { secure?: boolean } }> = [];
|
|
778
887
|
const handler = createClaudeCodeElicitationHandler({
|
|
779
888
|
input: async (_title: string, _placeholder?: string, opts?: { secure?: boolean }) => {
|
|
780
889
|
inputCalls.push({ opts });
|
|
781
|
-
return
|
|
890
|
+
return secureValue;
|
|
782
891
|
},
|
|
783
892
|
} as any);
|
|
784
893
|
assert.ok(handler);
|
|
@@ -787,7 +896,7 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
787
896
|
assert.deepEqual(result, {
|
|
788
897
|
action: "accept",
|
|
789
898
|
content: {
|
|
790
|
-
TEST_SECURE_FIELD:
|
|
899
|
+
TEST_SECURE_FIELD: secureValue,
|
|
791
900
|
},
|
|
792
901
|
});
|
|
793
902
|
assert.equal(inputCalls.length, 1);
|
|
@@ -481,10 +481,13 @@ export async function runPreDispatch(
|
|
|
481
481
|
);
|
|
482
482
|
} else if (state.phase === "blocked") {
|
|
483
483
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
deps.
|
|
484
|
+
// Pause instead of hard-stop so the session is resumable with `/gsd auto`.
|
|
485
|
+
// Hard-stop here was causing premature termination when slice dependencies
|
|
486
|
+
// were temporarily unresolvable (e.g. after reassessment added new slices).
|
|
487
|
+
await deps.pauseAuto(ctx, pi);
|
|
488
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
|
|
489
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
|
|
490
|
+
deps.logCmuxEvent(prefs, blockerMsg, "warning");
|
|
488
491
|
} else {
|
|
489
492
|
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
490
493
|
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
@@ -583,13 +586,23 @@ export async function runPreDispatch(
|
|
|
583
586
|
return { action: "break", reason: "milestone-complete" };
|
|
584
587
|
}
|
|
585
588
|
|
|
586
|
-
// Terminal: blocked
|
|
589
|
+
// Terminal: blocked — pause instead of hard-stop so the session is resumable.
|
|
587
590
|
if (state.phase === "blocked") {
|
|
588
591
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
592
|
+
if (s.currentUnit) {
|
|
593
|
+
await deps.closeoutUnit(
|
|
594
|
+
ctx,
|
|
595
|
+
s.basePath,
|
|
596
|
+
s.currentUnit.type,
|
|
597
|
+
s.currentUnit.id,
|
|
598
|
+
s.currentUnit.startedAt,
|
|
599
|
+
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
await deps.pauseAuto(ctx, pi);
|
|
603
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
|
|
604
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
|
|
605
|
+
deps.logCmuxEvent(prefs, blockerMsg, "warning");
|
|
593
606
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
594
607
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
|
|
595
608
|
return { action: "break", reason: "blocked" };
|
|
@@ -307,8 +307,11 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
307
307
|
{
|
|
308
308
|
name: "reassess-roadmap (post-completion)",
|
|
309
309
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
310
|
-
if (prefs?.phases?.skip_reassess
|
|
311
|
-
|
|
310
|
+
if (prefs?.phases?.skip_reassess) return null;
|
|
311
|
+
// Default reassess_after_slice to true — reassessment after slice completion
|
|
312
|
+
// is essential for roadmap integrity. Opt-out via explicit `false`.
|
|
313
|
+
const reassessEnabled = prefs?.phases?.reassess_after_slice ?? true;
|
|
314
|
+
if (!reassessEnabled) return null;
|
|
312
315
|
const needsReassess = await checkNeedsReassessment(basePath, mid, state);
|
|
313
316
|
if (!needsReassess) return null;
|
|
314
317
|
return {
|
|
@@ -877,11 +880,14 @@ export async function resolveDispatch(
|
|
|
877
880
|
}
|
|
878
881
|
}
|
|
879
882
|
|
|
880
|
-
// No rule matched — unhandled phase
|
|
883
|
+
// No rule matched — unhandled phase.
|
|
884
|
+
// Use level "warning" so the loop pauses (resumable) instead of hard-stopping.
|
|
885
|
+
// Hard-stop here was causing premature termination for transient phase gaps
|
|
886
|
+
// (e.g. after reassessment modifies the roadmap and state needs re-derivation).
|
|
881
887
|
return {
|
|
882
888
|
action: "stop",
|
|
883
889
|
reason: `Unhandled phase "${ctx.state.phase}" — run /gsd doctor to diagnose.`,
|
|
884
|
-
level: "
|
|
890
|
+
level: "warning",
|
|
885
891
|
matchedRule: "<no-match>",
|
|
886
892
|
};
|
|
887
893
|
}
|
|
@@ -15,6 +15,7 @@ import { resolveModelForComplexity, escalateTier, getEligibleModels, loadCapabil
|
|
|
15
15
|
import { getLedger, getProjectTotals } from "./metrics.js";
|
|
16
16
|
import { unitPhaseLabel } from "./auto-dashboard.js";
|
|
17
17
|
import { getSessionModelOverride } from "./session-model-override.js";
|
|
18
|
+
import { logWarning } from "./workflow-logger.js";
|
|
18
19
|
|
|
19
20
|
export interface ModelSelectionResult {
|
|
20
21
|
/** Routing metadata for metrics recording */
|
|
@@ -25,9 +26,7 @@ export interface ModelSelectionResult {
|
|
|
25
26
|
|
|
26
27
|
export function resolvePreferredModelConfig(
|
|
27
28
|
unitType: string,
|
|
28
|
-
autoModeStartModel: { provider: string; id: string } | null,
|
|
29
|
-
/** When false, only return explicit per-phase model configs — do not
|
|
30
|
-
* synthesize a routing ceiling from dynamic_routing.tier_models (#3962). */
|
|
29
|
+
autoModeStartModel: { provider: string; id: string; flatRateCtx?: FlatRateContext } | null,
|
|
31
30
|
isAutoMode = true,
|
|
32
31
|
) {
|
|
33
32
|
const explicitConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
@@ -41,7 +40,7 @@ export function resolvePreferredModelConfig(
|
|
|
41
40
|
if (!routingConfig.enabled || !routingConfig.tier_models) return undefined;
|
|
42
41
|
|
|
43
42
|
// Don't synthesize a routing config for flat-rate providers (#3453).
|
|
44
|
-
if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider)) return undefined;
|
|
43
|
+
if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx)) return undefined;
|
|
45
44
|
|
|
46
45
|
const ceilingModel = routingConfig.tier_models.heavy
|
|
47
46
|
?? (autoModeStartModel ? `${autoModeStartModel.provider}/${autoModeStartModel.id}` : undefined);
|
|
@@ -68,7 +67,7 @@ export async function selectAndApplyModel(
|
|
|
68
67
|
basePath: string,
|
|
69
68
|
prefs: GSDPreferences | undefined,
|
|
70
69
|
verbose: boolean,
|
|
71
|
-
autoModeStartModel: { provider: string; id: string } | null,
|
|
70
|
+
autoModeStartModel: { provider: string; id: string; flatRateCtx?: FlatRateContext } | null,
|
|
72
71
|
retryContext?: { isRetry: boolean; previousTier?: string },
|
|
73
72
|
/** When false (interactive/guided-flow), skip dynamic routing and use the session model.
|
|
74
73
|
* Dynamic routing only applies in auto-mode where cost optimization is expected. (#3962) */
|
|
@@ -79,6 +78,17 @@ export async function selectAndApplyModel(
|
|
|
79
78
|
const effectiveSessionModelOverride = sessionModelOverride === undefined
|
|
80
79
|
? getSessionModelOverride(ctx.sessionManager.getSessionId())
|
|
81
80
|
: (sessionModelOverride ?? undefined);
|
|
81
|
+
// Enrich the start model with a flat-rate context up front so routing
|
|
82
|
+
// synthesis and the dispatch-time guard see the same signals (built-in
|
|
83
|
+
// list + user `flat_rate_providers` preference + externalCli auto-
|
|
84
|
+
// detection). The dispatch-time primary-model check below builds its
|
|
85
|
+
// own per-provider context when it has a resolved primary model.
|
|
86
|
+
if (autoModeStartModel) {
|
|
87
|
+
autoModeStartModel = {
|
|
88
|
+
...autoModeStartModel,
|
|
89
|
+
flatRateCtx: buildFlatRateContext(autoModeStartModel.provider, ctx, prefs),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
82
92
|
const modelConfig = effectiveSessionModelOverride
|
|
83
93
|
? undefined
|
|
84
94
|
: resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode);
|
|
@@ -107,12 +117,16 @@ export async function selectAndApplyModel(
|
|
|
107
117
|
if (routingConfig.enabled) {
|
|
108
118
|
const primaryModel = resolveModelId(modelConfig.primary, availableModels, ctx.model?.provider);
|
|
109
119
|
if (primaryModel) {
|
|
110
|
-
|
|
120
|
+
const primaryFlatRateCtx = buildFlatRateContext(primaryModel.provider, ctx, prefs);
|
|
121
|
+
if (isFlatRateProvider(primaryModel.provider, primaryFlatRateCtx)) {
|
|
111
122
|
routingConfig.enabled = false;
|
|
112
123
|
}
|
|
113
124
|
} else if (
|
|
114
|
-
(autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
|
|
115
|
-
|| (ctx.model?.provider && isFlatRateProvider(
|
|
125
|
+
(autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx))
|
|
126
|
+
|| (ctx.model?.provider && isFlatRateProvider(
|
|
127
|
+
ctx.model.provider,
|
|
128
|
+
buildFlatRateContext(ctx.model.provider, ctx, prefs),
|
|
129
|
+
))
|
|
116
130
|
) {
|
|
117
131
|
// Primary model unresolvable but provider signals indicate flat-rate —
|
|
118
132
|
// disable routing to prevent quality degradation.
|
|
@@ -416,8 +430,68 @@ export function resolveModelId<T extends { id: string; provider: string }>(
|
|
|
416
430
|
* Uses case-insensitive matching with alias support to prevent fail-open on
|
|
417
431
|
* provider naming variations (e.g. "copilot" vs "github-copilot").
|
|
418
432
|
*/
|
|
419
|
-
const
|
|
433
|
+
const BUILTIN_FLAT_RATE = new Set(["github-copilot", "copilot", "claude-code"]);
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Optional context that lets callers extend flat-rate detection beyond the
|
|
437
|
+
* hard-coded built-in list. Either signal on its own is enough to classify
|
|
438
|
+
* a provider as flat-rate.
|
|
439
|
+
*/
|
|
440
|
+
export interface FlatRateContext {
|
|
441
|
+
/**
|
|
442
|
+
* Auth mode for the specific provider being checked, as returned by
|
|
443
|
+
* `ctx.modelRegistry.getProviderAuthMode(provider)`. Any provider that
|
|
444
|
+
* wraps a local CLI (externalCli) is, by definition, a flat-rate
|
|
445
|
+
* subscription wrapper — every request costs the same regardless of
|
|
446
|
+
* model, so dynamic routing only degrades quality.
|
|
447
|
+
*/
|
|
448
|
+
authMode?: "apiKey" | "oauth" | "externalCli" | "none";
|
|
449
|
+
/**
|
|
450
|
+
* Case-insensitive list of extra provider IDs the user has declared as
|
|
451
|
+
* flat-rate via `preferences.flat_rate_providers`. Used for private
|
|
452
|
+
* subscription-backed proxies and enterprise-gated deployments that the
|
|
453
|
+
* built-in list doesn't know about.
|
|
454
|
+
*/
|
|
455
|
+
userFlatRate?: readonly string[];
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function isFlatRateProvider(provider: string, opts?: FlatRateContext): boolean {
|
|
459
|
+
const p = provider.toLowerCase();
|
|
460
|
+
if (BUILTIN_FLAT_RATE.has(p)) return true;
|
|
461
|
+
if (opts?.userFlatRate?.some(id => id.toLowerCase() === p)) return true;
|
|
462
|
+
if (opts?.authMode === "externalCli") return true;
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
420
465
|
|
|
421
|
-
|
|
422
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Build a FlatRateContext for a given provider from live runtime state.
|
|
468
|
+
* Safe to call when ctx or prefs are undefined — missing pieces are
|
|
469
|
+
* treated as "no signal".
|
|
470
|
+
*/
|
|
471
|
+
export function buildFlatRateContext(
|
|
472
|
+
provider: string,
|
|
473
|
+
ctx?: { modelRegistry?: { getProviderAuthMode?: (p: string) => string } },
|
|
474
|
+
prefs?: { flat_rate_providers?: readonly string[] },
|
|
475
|
+
): FlatRateContext {
|
|
476
|
+
let authMode: FlatRateContext["authMode"];
|
|
477
|
+
const getAuthMode = ctx?.modelRegistry?.getProviderAuthMode;
|
|
478
|
+
if (typeof getAuthMode === "function") {
|
|
479
|
+
try {
|
|
480
|
+
const mode = getAuthMode(provider);
|
|
481
|
+
if (mode === "apiKey" || mode === "oauth" || mode === "externalCli" || mode === "none") {
|
|
482
|
+
authMode = mode;
|
|
483
|
+
}
|
|
484
|
+
} catch (err) {
|
|
485
|
+
// Registry lookup failure must never break flat-rate detection —
|
|
486
|
+
// fall through with authMode undefined and surface the cause.
|
|
487
|
+
logWarning(
|
|
488
|
+
"dispatch",
|
|
489
|
+
`flat-rate auth-mode lookup failed for ${provider}: ${err instanceof Error ? err.message : String(err)}`,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
authMode,
|
|
495
|
+
userFlatRate: prefs?.flat_rate_providers,
|
|
496
|
+
};
|
|
423
497
|
}
|
|
@@ -104,6 +104,7 @@ import {
|
|
|
104
104
|
updateSliceProgressCache,
|
|
105
105
|
unitVerb,
|
|
106
106
|
hideFooter,
|
|
107
|
+
describeNextUnit,
|
|
107
108
|
} from "./auto-dashboard.js";
|
|
108
109
|
import { existsSync, unlinkSync } from "node:fs";
|
|
109
110
|
import { join } from "node:path";
|
|
@@ -233,6 +234,18 @@ export function detectRogueFileWrites(
|
|
|
233
234
|
return rogues;
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
export const STEP_COMPLETE_FALLBACK_MESSAGE =
|
|
238
|
+
"Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).";
|
|
239
|
+
|
|
240
|
+
export function buildStepCompleteMessage(nextState: import("./types.js").GSDState): string {
|
|
241
|
+
if (nextState.phase === "complete") {
|
|
242
|
+
return "Step complete — milestone finished. Run /gsd status to review, or start the next milestone.";
|
|
243
|
+
}
|
|
244
|
+
const next = describeNextUnit(nextState);
|
|
245
|
+
return `Step complete. Next: ${next.label}\n`
|
|
246
|
+
+ `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
|
|
247
|
+
}
|
|
248
|
+
|
|
236
249
|
export interface PreVerificationOpts {
|
|
237
250
|
skipSettleDelay?: boolean;
|
|
238
251
|
skipWorktreeSync?: boolean;
|
|
@@ -619,6 +632,30 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
619
632
|
s.verificationRetryCount.set(retryKey, attempt);
|
|
620
633
|
|
|
621
634
|
if (attempt > MAX_VERIFICATION_RETRIES) {
|
|
635
|
+
// #4175: For complete-milestone, a blocker placeholder is harmful —
|
|
636
|
+
// the stub SUMMARY has no recovery value (milestone is terminal),
|
|
637
|
+
// it does not update DB status (so deriveState never advances),
|
|
638
|
+
// and it fools stopAuto's presence check into merging a milestone
|
|
639
|
+
// that was never legitimately completed. Pause auto-mode with a
|
|
640
|
+
// clear single failure signal and preserve the worktree branch.
|
|
641
|
+
if (s.currentUnit.type === "complete-milestone") {
|
|
642
|
+
debugLog("postUnit", {
|
|
643
|
+
phase: "artifact-verify-pause-complete-milestone",
|
|
644
|
+
unitType: s.currentUnit.type,
|
|
645
|
+
unitId: s.currentUnit.id,
|
|
646
|
+
attempt,
|
|
647
|
+
maxRetries: MAX_VERIFICATION_RETRIES,
|
|
648
|
+
});
|
|
649
|
+
s.verificationRetryCount.delete(retryKey);
|
|
650
|
+
s.pendingVerificationRetry = null;
|
|
651
|
+
ctx.ui.notify(
|
|
652
|
+
`Milestone ${s.currentUnit.id} verification failed after ${MAX_VERIFICATION_RETRIES} retries — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`,
|
|
653
|
+
"error",
|
|
654
|
+
);
|
|
655
|
+
await pauseAuto(ctx, pi);
|
|
656
|
+
return "dispatched";
|
|
657
|
+
}
|
|
658
|
+
|
|
622
659
|
// Retries exhausted — write a blocker placeholder so the pipeline
|
|
623
660
|
// can advance past this stuck unit (#2653).
|
|
624
661
|
debugLog("postUnit", {
|
|
@@ -1025,8 +1062,17 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
|
|
|
1025
1062
|
}
|
|
1026
1063
|
}
|
|
1027
1064
|
|
|
1028
|
-
// Step mode → show wizard instead of dispatch
|
|
1065
|
+
// Step mode → show wizard instead of dispatch.
|
|
1066
|
+
// Without this notify(), /gsd in step mode finishes a unit and silently
|
|
1067
|
+
// exits the loop, leaving the user with no hint to /clear and /gsd again.
|
|
1029
1068
|
if (s.stepMode) {
|
|
1069
|
+
try {
|
|
1070
|
+
const nextState = await deriveState(s.basePath);
|
|
1071
|
+
ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
|
|
1074
|
+
ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
|
|
1075
|
+
}
|
|
1030
1076
|
return "step-wizard";
|
|
1031
1077
|
}
|
|
1032
1078
|
|