gsd-pi 2.73.0 → 2.73.1-dev.6ddfa43
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.js +0 -47
- package/dist/help-text.js +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +9 -3
- package/dist/resources/extensions/gsd/auto-dispatch.js +5 -3
- package/dist/resources/extensions/gsd/auto-model-selection.js +54 -11
- package/dist/resources/extensions/gsd/auto-prompts.js +9 -6
- package/dist/resources/extensions/gsd/auto-start.js +20 -6
- package/dist/resources/extensions/gsd/auto.js +5 -1
- 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/bootstrap/system-context.js +6 -1
- package/dist/resources/extensions/gsd/crash-recovery.js +51 -0
- 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/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/startup-model-validation.js +8 -5
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- 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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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 +3 -3
- 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/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
- package/dist/web/standalone/.next/server/chunks/63.js +3 -3
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- 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/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- 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/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-f1e30ab6bb269149.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- 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/src/index.ts +4 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.js +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +27 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +175 -8
- package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +25 -68
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- 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 +51 -26
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +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 +95 -21
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +63 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -0
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +38 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +1 -1
- package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +198 -8
- package/packages/pi-coding-agent/src/core/model-resolver.ts +26 -70
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +62 -26
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +71 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +115 -26
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -4
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +23 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +5 -0
- package/src/resources/extensions/gsd/auto-model-selection.ts +85 -11
- package/src/resources/extensions/gsd/auto-prompts.ts +9 -3
- package/src/resources/extensions/gsd/auto-start.ts +27 -6
- package/src/resources/extensions/gsd/auto.ts +5 -0
- 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/bootstrap/system-context.ts +8 -1
- package/src/resources/extensions/gsd/crash-recovery.ts +59 -0
- 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/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/tests/auto-start-model-capture.test.ts +51 -2
- package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +235 -0
- 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/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/subagent-model-dispatch.test.ts +267 -0
- package/dist/web/standalone/.next/static/chunks/app/page-7115e62689b5fd84.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{KSZ2dcC3p4z6lOmUpPpzr → r6AvNu-aMwn4nwqjHqAfw}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{KSZ2dcC3p4z6lOmUpPpzr → r6AvNu-aMwn4nwqjHqAfw}/_ssgManifest.js +0 -0
|
@@ -343,6 +343,26 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
343
343
|
);
|
|
344
344
|
});
|
|
345
345
|
|
|
346
|
+
test("buildSdkOptions maps reasoning to effort for adaptive Claude Code models (#3917)", () => {
|
|
347
|
+
const options = buildSdkOptions("claude-sonnet-4-6", "test", undefined, { reasoning: "high" });
|
|
348
|
+
assert.equal(options.effort, "high");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("buildSdkOptions upgrades xhigh reasoning to max for opus 4.6 (#3917)", () => {
|
|
352
|
+
const options = buildSdkOptions("claude-opus-4-6", "test", undefined, { reasoning: "xhigh" });
|
|
353
|
+
assert.equal(options.effort, "max");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("buildSdkOptions omits effort when reasoning is undefined (#3917)", () => {
|
|
357
|
+
const options = buildSdkOptions("claude-sonnet-4-6", "test");
|
|
358
|
+
assert.equal("effort" in options, false);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("buildSdkOptions omits effort for non-adaptive Claude models (#3917)", () => {
|
|
362
|
+
const options = buildSdkOptions("claude-sonnet-4-20250514", "test", undefined, { reasoning: "high" });
|
|
363
|
+
assert.equal("effort" in options, false);
|
|
364
|
+
});
|
|
365
|
+
|
|
346
366
|
test("buildSdkOptions includes workflow MCP server config when env is set", () => {
|
|
347
367
|
const prev = {
|
|
348
368
|
GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
|
|
@@ -774,11 +794,12 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
774
794
|
},
|
|
775
795
|
};
|
|
776
796
|
|
|
797
|
+
const secureValue = "ui-collected-value";
|
|
777
798
|
const inputCalls: Array<{ opts?: { secure?: boolean } }> = [];
|
|
778
799
|
const handler = createClaudeCodeElicitationHandler({
|
|
779
800
|
input: async (_title: string, _placeholder?: string, opts?: { secure?: boolean }) => {
|
|
780
801
|
inputCalls.push({ opts });
|
|
781
|
-
return
|
|
802
|
+
return secureValue;
|
|
782
803
|
},
|
|
783
804
|
} as any);
|
|
784
805
|
assert.ok(handler);
|
|
@@ -787,7 +808,7 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
787
808
|
assert.deepEqual(result, {
|
|
788
809
|
action: "accept",
|
|
789
810
|
content: {
|
|
790
|
-
TEST_SECURE_FIELD:
|
|
811
|
+
TEST_SECURE_FIELD: secureValue,
|
|
791
812
|
},
|
|
792
813
|
});
|
|
793
814
|
assert.equal(inputCalls.length, 1);
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
checkNeedsReassessment,
|
|
53
53
|
checkNeedsRunUat,
|
|
54
54
|
} from "./auto-prompts.js";
|
|
55
|
+
import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
|
|
55
56
|
|
|
56
57
|
// ─── Types ────────────────────────────────────────────────────────────────
|
|
57
58
|
|
|
@@ -423,6 +424,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
423
424
|
midTitle,
|
|
424
425
|
researchReadySlices,
|
|
425
426
|
basePath,
|
|
427
|
+
resolveModelWithFallbacksForUnit("subagent")?.primary,
|
|
426
428
|
),
|
|
427
429
|
};
|
|
428
430
|
},
|
|
@@ -510,6 +512,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
510
512
|
sid,
|
|
511
513
|
sTitle,
|
|
512
514
|
basePath,
|
|
515
|
+
resolveModelWithFallbacksForUnit("subagent")?.primary,
|
|
513
516
|
),
|
|
514
517
|
};
|
|
515
518
|
},
|
|
@@ -548,6 +551,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
548
551
|
const sid = state.activeSlice.id;
|
|
549
552
|
const sTitle = state.activeSlice.title;
|
|
550
553
|
const maxParallel = reactiveConfig.max_parallel ?? 2;
|
|
554
|
+
const subagentModel = reactiveConfig.subagent_model ?? resolveModelWithFallbacksForUnit("subagent")?.primary;
|
|
551
555
|
|
|
552
556
|
// Dry-run mode: max_parallel=1 means graph is derived and logged but
|
|
553
557
|
// execution remains sequential
|
|
@@ -618,6 +622,7 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
618
622
|
sTitle,
|
|
619
623
|
selected,
|
|
620
624
|
basePath,
|
|
625
|
+
subagentModel,
|
|
621
626
|
),
|
|
622
627
|
};
|
|
623
628
|
} catch (err) {
|
|
@@ -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
|
}
|
|
@@ -1926,6 +1926,7 @@ export async function buildReassessRoadmapPrompt(
|
|
|
1926
1926
|
export async function buildReactiveExecutePrompt(
|
|
1927
1927
|
mid: string, midTitle: string, sid: string, sTitle: string,
|
|
1928
1928
|
readyTaskIds: string[], base: string,
|
|
1929
|
+
subagentModel?: string,
|
|
1929
1930
|
): Promise<string> {
|
|
1930
1931
|
const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
|
|
1931
1932
|
|
|
@@ -1970,10 +1971,11 @@ export async function buildReactiveExecutePrompt(
|
|
|
1970
1971
|
{ carryForwardPaths: depPaths },
|
|
1971
1972
|
);
|
|
1972
1973
|
|
|
1974
|
+
const modelSuffix = subagentModel ? ` with model: "${subagentModel}"` : "";
|
|
1973
1975
|
subagentSections.push([
|
|
1974
1976
|
`### ${tid}: ${tTitle}`,
|
|
1975
1977
|
"",
|
|
1976
|
-
|
|
1978
|
+
`Use this as the prompt for a \`subagent\` call${modelSuffix}:`,
|
|
1977
1979
|
"",
|
|
1978
1980
|
"```",
|
|
1979
1981
|
taskPrompt,
|
|
@@ -2049,15 +2051,17 @@ export async function buildParallelResearchSlicesPrompt(
|
|
|
2049
2051
|
midTitle: string,
|
|
2050
2052
|
slices: Array<{ id: string; title: string }>,
|
|
2051
2053
|
basePath: string,
|
|
2054
|
+
subagentModel?: string,
|
|
2052
2055
|
): Promise<string> {
|
|
2053
2056
|
// Build individual research-slice prompts for each slice
|
|
2054
2057
|
const subagentSections: string[] = [];
|
|
2058
|
+
const modelSuffix = subagentModel ? ` with model: "${subagentModel}"` : "";
|
|
2055
2059
|
for (const slice of slices) {
|
|
2056
2060
|
const slicePrompt = await buildResearchSlicePrompt(mid, midTitle, slice.id, slice.title, basePath);
|
|
2057
2061
|
subagentSections.push([
|
|
2058
2062
|
`### ${slice.id}: ${slice.title}`,
|
|
2059
2063
|
"",
|
|
2060
|
-
|
|
2064
|
+
`Use this as the prompt for a \`subagent\` call${modelSuffix} (agent: \`gsd-executor\` or the default agent):`,
|
|
2061
2065
|
"",
|
|
2062
2066
|
"```",
|
|
2063
2067
|
slicePrompt,
|
|
@@ -2077,6 +2081,7 @@ export async function buildParallelResearchSlicesPrompt(
|
|
|
2077
2081
|
export async function buildGateEvaluatePrompt(
|
|
2078
2082
|
mid: string, midTitle: string, sid: string, sTitle: string,
|
|
2079
2083
|
base: string,
|
|
2084
|
+
subagentModel?: string,
|
|
2080
2085
|
): Promise<string> {
|
|
2081
2086
|
// Pull only the gates this turn actually owns (Q3/Q4). Filter via the
|
|
2082
2087
|
// registry so that scope:"slice" gates owned by other turns (Q8) can't
|
|
@@ -2128,10 +2133,11 @@ export async function buildGateEvaluatePrompt(
|
|
|
2128
2133
|
"- `findings`: detailed markdown findings (or empty if omitted)",
|
|
2129
2134
|
].join("\n");
|
|
2130
2135
|
|
|
2136
|
+
const modelSuffix = subagentModel ? ` with model: "${subagentModel}"` : "";
|
|
2131
2137
|
subagentSections.push([
|
|
2132
2138
|
`### ${def.id}: ${def.question}`,
|
|
2133
2139
|
"",
|
|
2134
|
-
|
|
2140
|
+
`Use this as the prompt for a \`subagent\` call${modelSuffix}:`,
|
|
2135
2141
|
"",
|
|
2136
2142
|
"```",
|
|
2137
2143
|
subPrompt,
|
|
@@ -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
|
|
@@ -811,12 +825,19 @@ export async function bootstrapAutoSession(
|
|
|
811
825
|
? `${s.autoModeStartModel.provider}/${s.autoModeStartModel.id}`
|
|
812
826
|
: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "default";
|
|
813
827
|
|
|
814
|
-
// Flat-rate providers (e.g. GitHub Copilot, claude-code
|
|
815
|
-
//
|
|
816
|
-
|
|
828
|
+
// Flat-rate providers (e.g. GitHub Copilot, claude-code, user-declared
|
|
829
|
+
// subscription proxies, externalCli CLIs) suppress routing at dispatch
|
|
830
|
+
// time (#3453) — reflect that in the banner. Thread the same
|
|
831
|
+
// FlatRateContext used by selectAndApplyModel so user-declared
|
|
832
|
+
// flat-rate providers and externalCli auto-detection are respected.
|
|
833
|
+
const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
|
|
834
|
+
const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
|
|
817
835
|
const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
|
|
818
836
|
const effectivelyEnabled = routingConfig.enabled
|
|
819
|
-
&& !(effectiveProvider && isFlatRateProvider(
|
|
837
|
+
&& !(effectiveProvider && isFlatRateProvider(
|
|
838
|
+
effectiveProvider,
|
|
839
|
+
buildFlatRateContext(effectiveProvider, ctx, bannerPrefs),
|
|
840
|
+
));
|
|
820
841
|
|
|
821
842
|
// The actual ceiling may come from tier_models.heavy, not the start model.
|
|
822
843
|
const effectiveCeiling = (routingConfig.enabled && routingConfig.tier_models?.heavy)
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
readCrashLock,
|
|
53
53
|
isLockProcessAlive,
|
|
54
54
|
formatCrashInfo,
|
|
55
|
+
emitCrashRecoveredUnitEnd,
|
|
55
56
|
} from "./crash-recovery.js";
|
|
56
57
|
import {
|
|
57
58
|
acquireSessionLock,
|
|
@@ -1332,6 +1333,10 @@ export async function startAuto(
|
|
|
1332
1333
|
}
|
|
1333
1334
|
|
|
1334
1335
|
if (freshStartAssessment.lock) {
|
|
1336
|
+
// Emit a synthetic unit-end for any unit-start that has no closing event.
|
|
1337
|
+
// This closes the journal gap reported in #3348 where the worker wrote side
|
|
1338
|
+
// effects (SUMMARY.md, DB updates) but died before emitting unit-end.
|
|
1339
|
+
emitCrashRecoveredUnitEnd(base, freshStartAssessment.lock);
|
|
1335
1340
|
clearLock(base);
|
|
1336
1341
|
}
|
|
1337
1342
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crash-log.ts — Write crash diagnostics to ~/.gsd/crash/<timestamp>.log
|
|
3
|
+
*
|
|
4
|
+
* Zero cross-dependencies: only uses Node.js built-ins so it can be imported
|
|
5
|
+
* safely from uncaughtException / unhandledRejection handlers and from tests
|
|
6
|
+
* without pulling in the full extension dependency tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Write a crash log to ~/.gsd/crash/<timestamp>.log (or $GSD_HOME/crash/).
|
|
15
|
+
* Never throws — must be safe to call from any error handler.
|
|
16
|
+
*/
|
|
17
|
+
export function writeCrashLog(err: Error, source: string): void {
|
|
18
|
+
try {
|
|
19
|
+
const crashDir = join(process.env.GSD_HOME ?? join(homedir(), ".gsd"), "crash");
|
|
20
|
+
mkdirSync(crashDir, { recursive: true });
|
|
21
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
22
|
+
const logPath = join(crashDir, `${ts}.log`);
|
|
23
|
+
const lines = [
|
|
24
|
+
`[gsd] ${source}: ${err.message}`,
|
|
25
|
+
`timestamp: ${new Date().toISOString()}`,
|
|
26
|
+
`pid: ${process.pid}`,
|
|
27
|
+
err.stack ?? "(no stack trace available)",
|
|
28
|
+
"",
|
|
29
|
+
];
|
|
30
|
+
appendFileSync(logPath, lines.join("\n"));
|
|
31
|
+
} catch { /* never throw from crash handler */ }
|
|
32
|
+
}
|
|
@@ -11,6 +11,9 @@ import { registerJournalTools } from "./journal-tools.js";
|
|
|
11
11
|
import { registerQueryTools } from "./query-tools.js";
|
|
12
12
|
import { registerHooks } from "./register-hooks.js";
|
|
13
13
|
import { registerShortcuts } from "./register-shortcuts.js";
|
|
14
|
+
import { writeCrashLog } from "./crash-log.js";
|
|
15
|
+
|
|
16
|
+
export { writeCrashLog } from "./crash-log.js";
|
|
14
17
|
|
|
15
18
|
export function handleRecoverableExtensionProcessError(err: Error): boolean {
|
|
16
19
|
if ((err as NodeJS.ErrnoException).code === "EPIPE") {
|
|
@@ -33,16 +36,25 @@ export function handleRecoverableExtensionProcessError(err: Error): boolean {
|
|
|
33
36
|
function installEpipeGuard(): void {
|
|
34
37
|
if (!process.listeners("uncaughtException").some((listener) => listener.name === "_gsdEpipeGuard")) {
|
|
35
38
|
const _gsdEpipeGuard = (err: Error): void => {
|
|
36
|
-
if (handleRecoverableExtensionProcessError(err))
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
process.
|
|
42
|
-
if (err.stack) process.stderr.write(`${err.stack}\n`);
|
|
39
|
+
if (handleRecoverableExtensionProcessError(err)) return;
|
|
40
|
+
// Write crash log and exit cleanly for unrecoverable errors.
|
|
41
|
+
// Logging and continuing was the original double-fault fix (#3163), but
|
|
42
|
+
// continuing in an indeterminate state is worse than a clean exit (#3348).
|
|
43
|
+
writeCrashLog(err, "uncaughtException");
|
|
44
|
+
process.exit(1);
|
|
43
45
|
};
|
|
44
46
|
process.on("uncaughtException", _gsdEpipeGuard);
|
|
45
47
|
}
|
|
48
|
+
|
|
49
|
+
if (!process.listeners("unhandledRejection").some((listener) => listener.name === "_gsdRejectionGuard")) {
|
|
50
|
+
const _gsdRejectionGuard = (reason: unknown, _promise: Promise<unknown>): void => {
|
|
51
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
52
|
+
if (handleRecoverableExtensionProcessError(err)) return;
|
|
53
|
+
writeCrashLog(err, "unhandledRejection");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
};
|
|
56
|
+
process.on("unhandledRejection", _gsdRejectionGuard);
|
|
57
|
+
}
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
export function registerGsdExtension(pi: ExtensionAPI): void {
|
|
@@ -9,6 +9,7 @@ import { debugTime } from "../debug-logger.js";
|
|
|
9
9
|
import { loadPrompt, getTemplatesDir } from "../prompt-loader.js";
|
|
10
10
|
import { readForensicsMarker } from "../forensics.js";
|
|
11
11
|
import { resolveAllSkillReferences, renderPreferencesForSystemPrompt, loadEffectiveGSDPreferences } from "../preferences.js";
|
|
12
|
+
import { resolveModelWithFallbacksForUnit } from "../preferences-models.js";
|
|
12
13
|
import { resolveSkillReference } from "../preferences-skills.js";
|
|
13
14
|
import { resolveGsdRootFile, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, relSliceFile, relSlicePath, relTaskFile } from "../paths.js";
|
|
14
15
|
import { ensureCodebaseMapFresh, readCodebaseMap } from "../codebase-generator.js";
|
|
@@ -175,7 +176,13 @@ export async function buildBeforeAgentStartResult(
|
|
|
175
176
|
const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd(), event.prompt) : null;
|
|
176
177
|
|
|
177
178
|
const worktreeBlock = buildWorktreeContextBlock();
|
|
178
|
-
|
|
179
|
+
|
|
180
|
+
const subagentModelConfig = resolveModelWithFallbacksForUnit("subagent");
|
|
181
|
+
const subagentModelBlock = subagentModelConfig
|
|
182
|
+
? `\n\n## Subagent Model\n\nWhen spawning subagents via the \`subagent\` tool, always pass \`model: "${subagentModelConfig.primary}"\` in the tool call parameters. Never omit this — always specify it explicitly.`
|
|
183
|
+
: "";
|
|
184
|
+
|
|
185
|
+
const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${codebaseBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}${subagentModelBlock}`;
|
|
179
186
|
|
|
180
187
|
stopContextTimer({
|
|
181
188
|
systemPromptSize: fullSystem.length,
|
|
@@ -15,6 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { gsdRoot } from "./paths.js";
|
|
16
16
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
17
17
|
import { effectiveLockFile } from "./session-lock.js";
|
|
18
|
+
import { emitJournalEvent, queryJournal } from "./journal.js";
|
|
18
19
|
|
|
19
20
|
export interface LockData {
|
|
20
21
|
pid: number;
|
|
@@ -118,3 +119,61 @@ export function formatCrashInfo(lock: LockData): string {
|
|
|
118
119
|
|
|
119
120
|
return lines.join("\n");
|
|
120
121
|
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Emit a synthetic unit-end event for a unit that crashed without emitting its own.
|
|
125
|
+
*
|
|
126
|
+
* Queries the journal to find the most recent unit-start for the crashed unit.
|
|
127
|
+
* If a matching unit-end already exists (e.g. the hard timeout fired), this is a
|
|
128
|
+
* no-op. Called during crash recovery, before clearing the stale lock.
|
|
129
|
+
*
|
|
130
|
+
* Addresses the gap reported in #3348 where `unit-start` was emitted but no
|
|
131
|
+
* `unit-end` followed — side effects landed but the worker died before closeout.
|
|
132
|
+
*/
|
|
133
|
+
export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): void {
|
|
134
|
+
// Skip bootstrap / starting pseudo-units — they have no meaningful unit-start event.
|
|
135
|
+
if (!lock.unitType || !lock.unitId || lock.unitType === "starting") return;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const all = queryJournal(basePath);
|
|
139
|
+
|
|
140
|
+
// Find the most recent unit-start for this unitId
|
|
141
|
+
const starts = all.filter(
|
|
142
|
+
(e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId,
|
|
143
|
+
);
|
|
144
|
+
if (starts.length === 0) return;
|
|
145
|
+
|
|
146
|
+
const lastStart = starts[starts.length - 1];
|
|
147
|
+
|
|
148
|
+
// Check if a unit-end was already emitted (e.g. hard timeout fired after the crash)
|
|
149
|
+
const alreadyClosed = all.some(
|
|
150
|
+
(e) =>
|
|
151
|
+
e.eventType === "unit-end" &&
|
|
152
|
+
e.data?.unitId === lock.unitId &&
|
|
153
|
+
e.causedBy?.flowId === lastStart.flowId &&
|
|
154
|
+
e.causedBy?.seq === lastStart.seq,
|
|
155
|
+
);
|
|
156
|
+
if (alreadyClosed) return;
|
|
157
|
+
|
|
158
|
+
// Find the highest seq in this flow for monotonic ordering
|
|
159
|
+
const maxSeq = all
|
|
160
|
+
.filter((e) => e.flowId === lastStart.flowId)
|
|
161
|
+
.reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
|
|
162
|
+
|
|
163
|
+
emitJournalEvent(basePath, {
|
|
164
|
+
ts: new Date().toISOString(),
|
|
165
|
+
flowId: lastStart.flowId,
|
|
166
|
+
seq: maxSeq + 1,
|
|
167
|
+
eventType: "unit-end",
|
|
168
|
+
data: {
|
|
169
|
+
unitType: lock.unitType,
|
|
170
|
+
unitId: lock.unitId,
|
|
171
|
+
status: "crash-recovered",
|
|
172
|
+
artifactVerified: false,
|
|
173
|
+
},
|
|
174
|
+
causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
// Never throw from crash recovery path — journal failure must not block recovery
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -1564,6 +1564,23 @@ export interface TaskRow {
|
|
|
1564
1564
|
sequence: number;
|
|
1565
1565
|
}
|
|
1566
1566
|
|
|
1567
|
+
function parseTaskArrayColumn(raw: unknown): string[] {
|
|
1568
|
+
if (typeof raw !== "string" || raw.trim() === "") return [];
|
|
1569
|
+
|
|
1570
|
+
try {
|
|
1571
|
+
const parsed = JSON.parse(raw);
|
|
1572
|
+
if (Array.isArray(parsed)) return parsed.map((value) => String(value));
|
|
1573
|
+
if (parsed === null || parsed === undefined || parsed === "") return [];
|
|
1574
|
+
return [String(parsed)];
|
|
1575
|
+
} catch {
|
|
1576
|
+
// Older/corrupt rows may contain comma-separated strings instead of JSON.
|
|
1577
|
+
return raw
|
|
1578
|
+
.split(",")
|
|
1579
|
+
.map((value) => value.trim())
|
|
1580
|
+
.filter(Boolean);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1567
1584
|
function rowToTask(row: Record<string, unknown>): TaskRow {
|
|
1568
1585
|
const parseTaskArray = (value: unknown): string[] => {
|
|
1569
1586
|
if (Array.isArray(value)) {
|
|
@@ -1603,8 +1620,8 @@ function rowToTask(row: Record<string, unknown>): TaskRow {
|
|
|
1603
1620
|
blocker_discovered: (row["blocker_discovered"] as number) === 1,
|
|
1604
1621
|
deviations: row["deviations"] as string,
|
|
1605
1622
|
known_issues: row["known_issues"] as string,
|
|
1606
|
-
key_files:
|
|
1607
|
-
key_decisions:
|
|
1623
|
+
key_files: parseTaskArrayColumn(row["key_files"]),
|
|
1624
|
+
key_decisions: parseTaskArrayColumn(row["key_decisions"]),
|
|
1608
1625
|
full_summary_md: row["full_summary_md"] as string,
|
|
1609
1626
|
description: (row["description"] as string) ?? "",
|
|
1610
1627
|
estimate: (row["estimate"] as string) ?? "",
|
|
@@ -2200,6 +2217,39 @@ export function deleteSlice(milestoneId: string, sliceId: string): void {
|
|
|
2200
2217
|
});
|
|
2201
2218
|
}
|
|
2202
2219
|
|
|
2220
|
+
export function deleteMilestone(milestoneId: string): void {
|
|
2221
|
+
if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
2222
|
+
transaction(() => {
|
|
2223
|
+
currentDb!.prepare(
|
|
2224
|
+
`DELETE FROM verification_evidence WHERE milestone_id = :mid`,
|
|
2225
|
+
).run({ ":mid": milestoneId });
|
|
2226
|
+
currentDb!.prepare(
|
|
2227
|
+
`DELETE FROM quality_gates WHERE milestone_id = :mid`,
|
|
2228
|
+
).run({ ":mid": milestoneId });
|
|
2229
|
+
currentDb!.prepare(
|
|
2230
|
+
`DELETE FROM tasks WHERE milestone_id = :mid`,
|
|
2231
|
+
).run({ ":mid": milestoneId });
|
|
2232
|
+
currentDb!.prepare(
|
|
2233
|
+
`DELETE FROM slice_dependencies WHERE milestone_id = :mid`,
|
|
2234
|
+
).run({ ":mid": milestoneId });
|
|
2235
|
+
currentDb!.prepare(
|
|
2236
|
+
`DELETE FROM slices WHERE milestone_id = :mid`,
|
|
2237
|
+
).run({ ":mid": milestoneId });
|
|
2238
|
+
currentDb!.prepare(
|
|
2239
|
+
`DELETE FROM replan_history WHERE milestone_id = :mid`,
|
|
2240
|
+
).run({ ":mid": milestoneId });
|
|
2241
|
+
currentDb!.prepare(
|
|
2242
|
+
`DELETE FROM assessments WHERE milestone_id = :mid`,
|
|
2243
|
+
).run({ ":mid": milestoneId });
|
|
2244
|
+
currentDb!.prepare(
|
|
2245
|
+
`DELETE FROM artifacts WHERE milestone_id = :mid`,
|
|
2246
|
+
).run({ ":mid": milestoneId });
|
|
2247
|
+
currentDb!.prepare(
|
|
2248
|
+
`DELETE FROM milestones WHERE id = :mid`,
|
|
2249
|
+
).run({ ":mid": milestoneId });
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2203
2253
|
export function updateSliceFields(milestoneId: string, sliceId: string, fields: {
|
|
2204
2254
|
title?: string;
|
|
2205
2255
|
risk?: string;
|
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
} from "./paths.js";
|
|
21
21
|
import { invalidateAllCaches } from "./cache.js";
|
|
22
22
|
import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
|
|
23
|
-
import { getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
23
|
+
import { deleteMilestone, getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
24
|
+
import { removeWorktree } from "./worktree-manager.js";
|
|
24
25
|
import { logWarning } from "./workflow-logger.js";
|
|
25
26
|
|
|
26
27
|
// ─── Park ──────────────────────────────────────────────────────────────────
|
|
@@ -110,6 +111,15 @@ export function discardMilestone(basePath: string, milestoneId: string): boolean
|
|
|
110
111
|
const mDir = resolveMilestonePath(basePath, milestoneId);
|
|
111
112
|
if (!mDir || !existsSync(mDir)) return false;
|
|
112
113
|
|
|
114
|
+
try {
|
|
115
|
+
removeWorktree(basePath, milestoneId, {
|
|
116
|
+
branch: `milestone/${milestoneId}`,
|
|
117
|
+
deleteBranch: true,
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logWarning("engine", `discardMilestone worktree cleanup failed for ${milestoneId}: ${(err as Error).message}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
rmSync(mDir, { recursive: true, force: true });
|
|
114
124
|
|
|
115
125
|
// Prune from queue order if present
|
|
@@ -118,6 +128,14 @@ export function discardMilestone(basePath: string, milestoneId: string): boolean
|
|
|
118
128
|
saveQueueOrder(basePath, order.filter(id => id !== milestoneId));
|
|
119
129
|
}
|
|
120
130
|
|
|
131
|
+
if (isDbAvailable()) {
|
|
132
|
+
try {
|
|
133
|
+
deleteMilestone(milestoneId);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logWarning("engine", `discardMilestone DB cleanup failed for ${milestoneId}: ${(err as Error).message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
invalidateAllCaches();
|
|
122
140
|
return true;
|
|
123
141
|
}
|