gsd-pi 2.73.0-dev.e1c09f2 → 2.73.1-dev.088d28f
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/resource-loader.js +2 -2
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +9 -3
- 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-start.js +23 -6
- package/dist/resources/extensions/gsd/auto.js +13 -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/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 +36 -0
- 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 +11 -11
- 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 +11 -11
- 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 -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/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 +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/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 +62 -26
- 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 +12 -4
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +23 -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-start.ts +30 -6
- package/src/resources/extensions/gsd/auto.ts +10 -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/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 +46 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
- 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/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.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/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → nwYTvJZ1-hZIfw98d9Wfg}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{_XD_gUDcZNBbWV5rI8RgS → nwYTvJZ1-hZIfw98d9Wfg}/_ssgManifest.js +0 -0
package/dist/resource-loader.js
CHANGED
|
@@ -358,7 +358,7 @@ function reconcileMergedNodeModules(agentNodeModules, hoisted, internal) {
|
|
|
358
358
|
if (entry.name.startsWith('.'))
|
|
359
359
|
continue;
|
|
360
360
|
try {
|
|
361
|
-
symlinkSync(join(hoisted, entry.name), join(agentNodeModules, entry.name));
|
|
361
|
+
symlinkSync(join(hoisted, entry.name), join(agentNodeModules, entry.name), 'junction');
|
|
362
362
|
linkedCount++;
|
|
363
363
|
}
|
|
364
364
|
catch { /* skip individual */ }
|
|
@@ -382,7 +382,7 @@ function reconcileMergedNodeModules(agentNodeModules, hoisted, internal) {
|
|
|
382
382
|
}
|
|
383
383
|
catch { /* didn't exist — will create below */ }
|
|
384
384
|
try {
|
|
385
|
-
symlinkSync(join(internal, entry.name), link);
|
|
385
|
+
symlinkSync(join(internal, entry.name), link, 'junction');
|
|
386
386
|
linkedCount++;
|
|
387
387
|
}
|
|
388
388
|
catch { /* skip individual */ }
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* AssistantMessageEvents for TUI rendering, then strips tool-call blocks from
|
|
7
7
|
* the final AssistantMessage so GSD's agent loop doesn't try to dispatch them.
|
|
8
8
|
*/
|
|
9
|
-
import { EventStream } from "@gsd/pi-ai";
|
|
9
|
+
import { EventStream, mapThinkingLevelToEffort, supportsAdaptiveThinking } from "@gsd/pi-ai";
|
|
10
10
|
import { execSync } from "node:child_process";
|
|
11
11
|
import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js";
|
|
12
12
|
import { buildWorkflowMcpServers } from "../gsd/workflow-mcp.js";
|
|
@@ -437,6 +437,7 @@ export async function resolveClaudePermissionMode(env = process.env) {
|
|
|
437
437
|
* behaviour pass `permissionMode: "bypassPermissions"` explicitly.
|
|
438
438
|
*/
|
|
439
439
|
export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
|
|
440
|
+
const { reasoning, ...sdkExtraOptions } = extraOptions;
|
|
440
441
|
const mcpServers = buildWorkflowMcpServers();
|
|
441
442
|
const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
|
|
442
443
|
const disallowedTools = ["AskUserQuestion"];
|
|
@@ -455,6 +456,9 @@ export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
|
|
|
455
456
|
"Bash(pwd)",
|
|
456
457
|
...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
|
|
457
458
|
];
|
|
459
|
+
const effort = reasoning && supportsAdaptiveThinking(modelId)
|
|
460
|
+
? mapThinkingLevelToEffort(reasoning, modelId)
|
|
461
|
+
: undefined;
|
|
458
462
|
return {
|
|
459
463
|
pathToClaudeCodeExecutable: getClaudePath(),
|
|
460
464
|
model: modelId,
|
|
@@ -469,7 +473,8 @@ export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
|
|
|
469
473
|
...(allowedTools.length > 0 ? { allowedTools } : {}),
|
|
470
474
|
...(mcpServers ? { mcpServers } : {}),
|
|
471
475
|
betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
|
|
472
|
-
...
|
|
476
|
+
...(effort ? { effort } : {}),
|
|
477
|
+
...sdkExtraOptions,
|
|
473
478
|
};
|
|
474
479
|
}
|
|
475
480
|
function normalizeToolResultContent(content) {
|
|
@@ -620,9 +625,10 @@ async function pumpSdkMessages(model, context, options, stream) {
|
|
|
620
625
|
const permissionMode = await resolveClaudePermissionMode();
|
|
621
626
|
const sdkOpts = buildSdkOptions(modelId, prompt, { permissionMode }, typeof options?.extensionUIContext === "object"
|
|
622
627
|
? {
|
|
628
|
+
reasoning: options?.reasoning,
|
|
623
629
|
onElicitation: createClaudeCodeElicitationHandler(options?.extensionUIContext),
|
|
624
630
|
}
|
|
625
|
-
: {});
|
|
631
|
+
: { reasoning: options?.reasoning });
|
|
626
632
|
const queryResult = sdk.query({
|
|
627
633
|
prompt,
|
|
628
634
|
options: {
|
|
@@ -320,10 +320,13 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
320
320
|
}
|
|
321
321
|
else if (state.phase === "blocked") {
|
|
322
322
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
deps.
|
|
323
|
+
// Pause instead of hard-stop so the session is resumable with `/gsd auto`.
|
|
324
|
+
// Hard-stop here was causing premature termination when slice dependencies
|
|
325
|
+
// were temporarily unresolvable (e.g. after reassessment added new slices).
|
|
326
|
+
await deps.pauseAuto(ctx, pi);
|
|
327
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
|
|
328
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
|
|
329
|
+
deps.logCmuxEvent(prefs, blockerMsg, "warning");
|
|
327
330
|
}
|
|
328
331
|
else {
|
|
329
332
|
const ids = incomplete.map((m) => m.id).join(", ");
|
|
@@ -392,13 +395,16 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
392
395
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "milestone-complete", milestoneId: mid } });
|
|
393
396
|
return { action: "break", reason: "milestone-complete" };
|
|
394
397
|
}
|
|
395
|
-
// Terminal: blocked
|
|
398
|
+
// Terminal: blocked — pause instead of hard-stop so the session is resumable.
|
|
396
399
|
if (state.phase === "blocked") {
|
|
397
400
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
deps.
|
|
401
|
+
if (s.currentUnit) {
|
|
402
|
+
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
403
|
+
}
|
|
404
|
+
await deps.pauseAuto(ctx, pi);
|
|
405
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
|
|
406
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
|
|
407
|
+
deps.logCmuxEvent(prefs, blockerMsg, "warning");
|
|
402
408
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
403
409
|
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
|
|
404
410
|
return { action: "break", reason: "blocked" };
|
|
@@ -216,7 +216,12 @@ export const DISPATCH_RULES = [
|
|
|
216
216
|
{
|
|
217
217
|
name: "reassess-roadmap (post-completion)",
|
|
218
218
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
219
|
-
if (prefs?.phases?.skip_reassess
|
|
219
|
+
if (prefs?.phases?.skip_reassess)
|
|
220
|
+
return null;
|
|
221
|
+
// Default reassess_after_slice to true — reassessment after slice completion
|
|
222
|
+
// is essential for roadmap integrity. Opt-out via explicit `false`.
|
|
223
|
+
const reassessEnabled = prefs?.phases?.reassess_after_slice ?? true;
|
|
224
|
+
if (!reassessEnabled)
|
|
220
225
|
return null;
|
|
221
226
|
const needsReassess = await checkNeedsReassessment(basePath, mid, state);
|
|
222
227
|
if (!needsReassess)
|
|
@@ -710,11 +715,14 @@ export async function resolveDispatch(ctx) {
|
|
|
710
715
|
return result;
|
|
711
716
|
}
|
|
712
717
|
}
|
|
713
|
-
// No rule matched — unhandled phase
|
|
718
|
+
// No rule matched — unhandled phase.
|
|
719
|
+
// Use level "warning" so the loop pauses (resumable) instead of hard-stopping.
|
|
720
|
+
// Hard-stop here was causing premature termination for transient phase gaps
|
|
721
|
+
// (e.g. after reassessment modifies the roadmap and state needs re-derivation).
|
|
714
722
|
return {
|
|
715
723
|
action: "stop",
|
|
716
724
|
reason: `Unhandled phase "${ctx.state.phase}" — run /gsd doctor to diagnose.`,
|
|
717
|
-
level: "
|
|
725
|
+
level: "warning",
|
|
718
726
|
matchedRule: "<no-match>",
|
|
719
727
|
};
|
|
720
728
|
}
|
|
@@ -9,10 +9,8 @@ import { resolveModelForComplexity, escalateTier, getEligibleModels, loadCapabil
|
|
|
9
9
|
import { getLedger, getProjectTotals } from "./metrics.js";
|
|
10
10
|
import { unitPhaseLabel } from "./auto-dashboard.js";
|
|
11
11
|
import { getSessionModelOverride } from "./session-model-override.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* synthesize a routing ceiling from dynamic_routing.tier_models (#3962). */
|
|
15
|
-
isAutoMode = true) {
|
|
12
|
+
import { logWarning } from "./workflow-logger.js";
|
|
13
|
+
export function resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode = true) {
|
|
16
14
|
const explicitConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
17
15
|
if (explicitConfig)
|
|
18
16
|
return explicitConfig;
|
|
@@ -24,7 +22,7 @@ isAutoMode = true) {
|
|
|
24
22
|
if (!routingConfig.enabled || !routingConfig.tier_models)
|
|
25
23
|
return undefined;
|
|
26
24
|
// Don't synthesize a routing config for flat-rate providers (#3453).
|
|
27
|
-
if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
|
|
25
|
+
if (autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx))
|
|
28
26
|
return undefined;
|
|
29
27
|
const ceilingModel = routingConfig.tier_models.heavy
|
|
30
28
|
?? (autoModeStartModel ? `${autoModeStartModel.provider}/${autoModeStartModel.id}` : undefined);
|
|
@@ -51,6 +49,17 @@ sessionModelOverride) {
|
|
|
51
49
|
const effectiveSessionModelOverride = sessionModelOverride === undefined
|
|
52
50
|
? getSessionModelOverride(ctx.sessionManager.getSessionId())
|
|
53
51
|
: (sessionModelOverride ?? undefined);
|
|
52
|
+
// Enrich the start model with a flat-rate context up front so routing
|
|
53
|
+
// synthesis and the dispatch-time guard see the same signals (built-in
|
|
54
|
+
// list + user `flat_rate_providers` preference + externalCli auto-
|
|
55
|
+
// detection). The dispatch-time primary-model check below builds its
|
|
56
|
+
// own per-provider context when it has a resolved primary model.
|
|
57
|
+
if (autoModeStartModel) {
|
|
58
|
+
autoModeStartModel = {
|
|
59
|
+
...autoModeStartModel,
|
|
60
|
+
flatRateCtx: buildFlatRateContext(autoModeStartModel.provider, ctx, prefs),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
54
63
|
const modelConfig = effectiveSessionModelOverride
|
|
55
64
|
? undefined
|
|
56
65
|
: resolvePreferredModelConfig(unitType, autoModeStartModel, isAutoMode);
|
|
@@ -76,12 +85,13 @@ sessionModelOverride) {
|
|
|
76
85
|
if (routingConfig.enabled) {
|
|
77
86
|
const primaryModel = resolveModelId(modelConfig.primary, availableModels, ctx.model?.provider);
|
|
78
87
|
if (primaryModel) {
|
|
79
|
-
|
|
88
|
+
const primaryFlatRateCtx = buildFlatRateContext(primaryModel.provider, ctx, prefs);
|
|
89
|
+
if (isFlatRateProvider(primaryModel.provider, primaryFlatRateCtx)) {
|
|
80
90
|
routingConfig.enabled = false;
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
|
-
else if ((autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider))
|
|
84
|
-
|| (ctx.model?.provider && isFlatRateProvider(ctx.model.provider))) {
|
|
93
|
+
else if ((autoModeStartModel && isFlatRateProvider(autoModeStartModel.provider, autoModeStartModel.flatRateCtx))
|
|
94
|
+
|| (ctx.model?.provider && isFlatRateProvider(ctx.model.provider, buildFlatRateContext(ctx.model.provider, ctx, prefs)))) {
|
|
85
95
|
// Primary model unresolvable but provider signals indicate flat-rate —
|
|
86
96
|
// disable routing to prevent quality degradation.
|
|
87
97
|
routingConfig.enabled = false;
|
|
@@ -331,7 +341,40 @@ export function resolveModelId(modelId, availableModels, currentProvider) {
|
|
|
331
341
|
* Uses case-insensitive matching with alias support to prevent fail-open on
|
|
332
342
|
* provider naming variations (e.g. "copilot" vs "github-copilot").
|
|
333
343
|
*/
|
|
334
|
-
const
|
|
335
|
-
export function isFlatRateProvider(provider) {
|
|
336
|
-
|
|
344
|
+
const BUILTIN_FLAT_RATE = new Set(["github-copilot", "copilot", "claude-code"]);
|
|
345
|
+
export function isFlatRateProvider(provider, opts) {
|
|
346
|
+
const p = provider.toLowerCase();
|
|
347
|
+
if (BUILTIN_FLAT_RATE.has(p))
|
|
348
|
+
return true;
|
|
349
|
+
if (opts?.userFlatRate?.some(id => id.toLowerCase() === p))
|
|
350
|
+
return true;
|
|
351
|
+
if (opts?.authMode === "externalCli")
|
|
352
|
+
return true;
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Build a FlatRateContext for a given provider from live runtime state.
|
|
357
|
+
* Safe to call when ctx or prefs are undefined — missing pieces are
|
|
358
|
+
* treated as "no signal".
|
|
359
|
+
*/
|
|
360
|
+
export function buildFlatRateContext(provider, ctx, prefs) {
|
|
361
|
+
let authMode;
|
|
362
|
+
const getAuthMode = ctx?.modelRegistry?.getProviderAuthMode;
|
|
363
|
+
if (typeof getAuthMode === "function") {
|
|
364
|
+
try {
|
|
365
|
+
const mode = getAuthMode(provider);
|
|
366
|
+
if (mode === "apiKey" || mode === "oauth" || mode === "externalCli" || mode === "none") {
|
|
367
|
+
authMode = mode;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
// Registry lookup failure must never break flat-rate detection —
|
|
372
|
+
// fall through with authMode undefined and surface the cause.
|
|
373
|
+
logWarning("dispatch", `flat-rate auth-mode lookup failed for ${provider}: ${err instanceof Error ? err.message : String(err)}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
authMode,
|
|
378
|
+
userFlatRate: prefs?.flat_rate_providers,
|
|
379
|
+
};
|
|
337
380
|
}
|
|
@@ -38,7 +38,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync, } fro
|
|
|
38
38
|
import { join } from "node:path";
|
|
39
39
|
import { sep as pathSep } from "node:path";
|
|
40
40
|
import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
|
|
41
|
-
import { resolveDefaultSessionModel, resolveDynamicRoutingConfig } from "./preferences-models.js";
|
|
41
|
+
import { isCustomProvider, resolveDefaultSessionModel, resolveDynamicRoutingConfig, } from "./preferences-models.js";
|
|
42
42
|
import { getSessionModelOverride } from "./session-model-override.js";
|
|
43
43
|
/**
|
|
44
44
|
* Bootstrap a fresh auto-mode session. Handles everything from git init
|
|
@@ -195,8 +195,18 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
195
195
|
//
|
|
196
196
|
// This preserves #3517 defaults while honoring explicit runtime model
|
|
197
197
|
// selection for subsequent /gsd runs in the same session.
|
|
198
|
+
//
|
|
199
|
+
// Exception (#4122): when the session provider is a custom provider declared
|
|
200
|
+
// in ~/.gsd/agent/models.json (Ollama, vLLM, OpenAI-compatible proxy, etc.),
|
|
201
|
+
// PREFERENCES.md is skipped entirely. PREFERENCES.md cannot reference custom
|
|
202
|
+
// providers, so honoring it would silently reroute auto-mode to a built-in
|
|
203
|
+
// provider the user is not logged into and surface as "Not logged in · Please
|
|
204
|
+
// run /login" before pausing and resetting to claude-code/claude-sonnet-4-6.
|
|
198
205
|
const manualSessionOverride = getSessionModelOverride(ctx.sessionManager.getSessionId());
|
|
199
|
-
const
|
|
206
|
+
const sessionProviderIsCustom = isCustomProvider(ctx.model?.provider);
|
|
207
|
+
const preferredModel = sessionProviderIsCustom
|
|
208
|
+
? null
|
|
209
|
+
: resolveDefaultSessionModel(ctx.model?.provider);
|
|
200
210
|
// Validate the preferred model against the live registry + provider auth so
|
|
201
211
|
// an unconfigured PREFERENCES.md entry (no API key / OAuth) can't become the
|
|
202
212
|
// start-model snapshot. Without this, every subsequent unit would try to
|
|
@@ -622,6 +632,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
622
632
|
}
|
|
623
633
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
624
634
|
ctx.ui.setFooter(hideFooter);
|
|
635
|
+
// Hide gsd-health during AUTO — gsd-progress is the single source of truth
|
|
636
|
+
// for last-commit / cost / health signal while auto is running.
|
|
637
|
+
ctx.ui.setWidget("gsd-health", undefined);
|
|
625
638
|
const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
|
|
626
639
|
const pendingCount = (state.registry ?? []).filter((m) => m.status !== "complete" && m.status !== "parked").length;
|
|
627
640
|
const scopeMsg = pendingCount > 1
|
|
@@ -636,12 +649,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
636
649
|
const startModelLabel = s.autoModeStartModel
|
|
637
650
|
? `${s.autoModeStartModel.provider}/${s.autoModeStartModel.id}`
|
|
638
651
|
: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "default";
|
|
639
|
-
// Flat-rate providers (e.g. GitHub Copilot, claude-code
|
|
640
|
-
//
|
|
641
|
-
|
|
652
|
+
// Flat-rate providers (e.g. GitHub Copilot, claude-code, user-declared
|
|
653
|
+
// subscription proxies, externalCli CLIs) suppress routing at dispatch
|
|
654
|
+
// time (#3453) — reflect that in the banner. Thread the same
|
|
655
|
+
// FlatRateContext used by selectAndApplyModel so user-declared
|
|
656
|
+
// flat-rate providers and externalCli auto-detection are respected.
|
|
657
|
+
const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
|
|
658
|
+
const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
|
|
642
659
|
const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
|
|
643
660
|
const effectivelyEnabled = routingConfig.enabled
|
|
644
|
-
&& !(effectiveProvider && isFlatRateProvider(effectiveProvider));
|
|
661
|
+
&& !(effectiveProvider && isFlatRateProvider(effectiveProvider, buildFlatRateContext(effectiveProvider, ctx, bannerPrefs)));
|
|
645
662
|
// The actual ceiling may come from tier_models.heavy, not the start model.
|
|
646
663
|
const effectiveCeiling = (routingConfig.enabled && routingConfig.tier_models?.heavy)
|
|
647
664
|
? routingConfig.tier_models.heavy
|
|
@@ -19,7 +19,7 @@ import { gsdRoot, resolveMilestoneFile, resolveMilestonePath, resolveDir, milest
|
|
|
19
19
|
import { invalidateAllCaches } from "./cache.js";
|
|
20
20
|
import { clearActivityLogState } from "./activity-log.js";
|
|
21
21
|
import { synthesizeCrashRecovery, getDeepDiagnostic, readActiveMilestoneId, } from "./session-forensics.js";
|
|
22
|
-
import { writeLock, clearLock, readCrashLock, isLockProcessAlive, formatCrashInfo, } from "./crash-recovery.js";
|
|
22
|
+
import { writeLock, clearLock, readCrashLock, isLockProcessAlive, formatCrashInfo, emitCrashRecoveredUnitEnd, } from "./crash-recovery.js";
|
|
23
23
|
import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
|
|
24
24
|
import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, } from "./preferences.js";
|
|
25
25
|
import { sendDesktopNotification } from "./notifications.js";
|
|
@@ -63,6 +63,7 @@ import { startUnitSupervision } from "./auto-timers.js";
|
|
|
63
63
|
import { runPostUnitVerification } from "./auto-verification.js";
|
|
64
64
|
import { postUnitPreVerification, postUnitPostVerification, } from "./auto-post-unit.js";
|
|
65
65
|
import { bootstrapAutoSession, openProjectDbIfPresent } from "./auto-start.js";
|
|
66
|
+
import { initHealthWidget } from "./health-widget.js";
|
|
66
67
|
import { autoLoop, resolveAgentEnd, resolveAgentEndCancelled, _resetPendingResolve, isSessionSwitchInFlight } from "./auto-loop.js";
|
|
67
68
|
import { WorktreeResolver, } from "./worktree-resolver.js";
|
|
68
69
|
import { reorderForCaching } from "./prompt-ordering.js";
|
|
@@ -397,6 +398,8 @@ function handleLostSessionLock(ctx, lockStatus) {
|
|
|
397
398
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
398
399
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
399
400
|
ctx?.ui.setFooter(undefined);
|
|
401
|
+
if (ctx)
|
|
402
|
+
initHealthWidget(ctx);
|
|
400
403
|
}
|
|
401
404
|
/**
|
|
402
405
|
* Lightweight cleanup after autoLoop exits via step-wizard break.
|
|
@@ -431,6 +434,7 @@ function cleanupAfterLoopExit(ctx) {
|
|
|
431
434
|
ctx.ui.setStatus("gsd-auto", undefined);
|
|
432
435
|
ctx.ui.setWidget("gsd-progress", undefined);
|
|
433
436
|
ctx.ui.setFooter(undefined);
|
|
437
|
+
initHealthWidget(ctx);
|
|
434
438
|
}
|
|
435
439
|
// Restore CWD out of worktree back to original project root
|
|
436
440
|
if (s.originalBasePath) {
|
|
@@ -676,6 +680,8 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
676
680
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
677
681
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
678
682
|
ctx?.ui.setFooter(undefined);
|
|
683
|
+
if (ctx)
|
|
684
|
+
initHealthWidget(ctx);
|
|
679
685
|
restoreProjectRootEnv();
|
|
680
686
|
restoreMilestoneLockEnv();
|
|
681
687
|
// Reset all session state in one call
|
|
@@ -762,6 +768,8 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
|
|
|
762
768
|
ctx?.ui.setStatus("gsd-auto", "paused");
|
|
763
769
|
ctx?.ui.setWidget("gsd-progress", undefined);
|
|
764
770
|
ctx?.ui.setFooter(undefined);
|
|
771
|
+
if (ctx)
|
|
772
|
+
initHealthWidget(ctx);
|
|
765
773
|
const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
|
|
766
774
|
ctx?.ui.notify(`${s.stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`, "info");
|
|
767
775
|
}
|
|
@@ -1014,6 +1022,10 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
1014
1022
|
s.stepMode = requestedStepMode;
|
|
1015
1023
|
}
|
|
1016
1024
|
if (freshStartAssessment.lock) {
|
|
1025
|
+
// Emit a synthetic unit-end for any unit-start that has no closing event.
|
|
1026
|
+
// This closes the journal gap reported in #3348 where the worker wrote side
|
|
1027
|
+
// effects (SUMMARY.md, DB updates) but died before emitting unit-end.
|
|
1028
|
+
emitCrashRecoveredUnitEnd(base, freshStartAssessment.lock);
|
|
1017
1029
|
clearLock(base);
|
|
1018
1030
|
}
|
|
1019
1031
|
if (!s.paused) {
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
/**
|
|
12
|
+
* Write a crash log to ~/.gsd/crash/<timestamp>.log (or $GSD_HOME/crash/).
|
|
13
|
+
* Never throws — must be safe to call from any error handler.
|
|
14
|
+
*/
|
|
15
|
+
export function writeCrashLog(err, source) {
|
|
16
|
+
try {
|
|
17
|
+
const crashDir = join(process.env.GSD_HOME ?? join(homedir(), ".gsd"), "crash");
|
|
18
|
+
mkdirSync(crashDir, { recursive: true });
|
|
19
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
20
|
+
const logPath = join(crashDir, `${ts}.log`);
|
|
21
|
+
const lines = [
|
|
22
|
+
`[gsd] ${source}: ${err.message}`,
|
|
23
|
+
`timestamp: ${new Date().toISOString()}`,
|
|
24
|
+
`pid: ${process.pid}`,
|
|
25
|
+
err.stack ?? "(no stack trace available)",
|
|
26
|
+
"",
|
|
27
|
+
];
|
|
28
|
+
appendFileSync(logPath, lines.join("\n"));
|
|
29
|
+
}
|
|
30
|
+
catch { /* never throw from crash handler */ }
|
|
31
|
+
}
|
|
@@ -8,6 +8,8 @@ import { registerJournalTools } from "./journal-tools.js";
|
|
|
8
8
|
import { registerQueryTools } from "./query-tools.js";
|
|
9
9
|
import { registerHooks } from "./register-hooks.js";
|
|
10
10
|
import { registerShortcuts } from "./register-shortcuts.js";
|
|
11
|
+
import { writeCrashLog } from "./crash-log.js";
|
|
12
|
+
export { writeCrashLog } from "./crash-log.js";
|
|
11
13
|
export function handleRecoverableExtensionProcessError(err) {
|
|
12
14
|
if (err.code === "EPIPE") {
|
|
13
15
|
process.exit(0);
|
|
@@ -28,17 +30,26 @@ export function handleRecoverableExtensionProcessError(err) {
|
|
|
28
30
|
function installEpipeGuard() {
|
|
29
31
|
if (!process.listeners("uncaughtException").some((listener) => listener.name === "_gsdEpipeGuard")) {
|
|
30
32
|
const _gsdEpipeGuard = (err) => {
|
|
31
|
-
if (handleRecoverableExtensionProcessError(err))
|
|
33
|
+
if (handleRecoverableExtensionProcessError(err))
|
|
32
34
|
return;
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
process.stderr.write(`${err.stack}\n`);
|
|
35
|
+
// Write crash log and exit cleanly for unrecoverable errors.
|
|
36
|
+
// Logging and continuing was the original double-fault fix (#3163), but
|
|
37
|
+
// continuing in an indeterminate state is worse than a clean exit (#3348).
|
|
38
|
+
writeCrashLog(err, "uncaughtException");
|
|
39
|
+
process.exit(1);
|
|
39
40
|
};
|
|
40
41
|
process.on("uncaughtException", _gsdEpipeGuard);
|
|
41
42
|
}
|
|
43
|
+
if (!process.listeners("unhandledRejection").some((listener) => listener.name === "_gsdRejectionGuard")) {
|
|
44
|
+
const _gsdRejectionGuard = (reason, _promise) => {
|
|
45
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
46
|
+
if (handleRecoverableExtensionProcessError(err))
|
|
47
|
+
return;
|
|
48
|
+
writeCrashLog(err, "unhandledRejection");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
};
|
|
51
|
+
process.on("unhandledRejection", _gsdRejectionGuard);
|
|
52
|
+
}
|
|
42
53
|
}
|
|
43
54
|
export function registerGsdExtension(pi) {
|
|
44
55
|
registerGSDCommand(pi);
|
|
@@ -17,6 +17,11 @@ import { projectRoot } from "./commands/context.js";
|
|
|
17
17
|
import { loadPrompt } from "./prompt-loader.js";
|
|
18
18
|
const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/gsd-pi/latest";
|
|
19
19
|
const UPDATE_FETCH_TIMEOUT_MS = 5000;
|
|
20
|
+
function resolveInstallCommand(pkg) {
|
|
21
|
+
if ('bun' in process.versions)
|
|
22
|
+
return `bun add -g ${pkg}`;
|
|
23
|
+
return `npm install -g ${pkg}`;
|
|
24
|
+
}
|
|
20
25
|
async function fetchLatestVersionForCommand() {
|
|
21
26
|
const controller = new AbortController();
|
|
22
27
|
const timeout = setTimeout(() => controller.abort(), UPDATE_FETCH_TIMEOUT_MS);
|
|
@@ -344,13 +349,14 @@ export async function handleUpdate(ctx) {
|
|
|
344
349
|
return;
|
|
345
350
|
}
|
|
346
351
|
ctx.ui.notify(`Updating: v${current} → v${latest}...`, "info");
|
|
352
|
+
const installCmd = resolveInstallCommand(`${NPM_PACKAGE}@latest`);
|
|
347
353
|
try {
|
|
348
|
-
execSync(
|
|
354
|
+
execSync(installCmd, {
|
|
349
355
|
stdio: ["ignore", "pipe", "ignore"],
|
|
350
356
|
});
|
|
351
357
|
ctx.ui.notify(`Updated to v${latest}. Restart your GSD session to use the new version.`, "info");
|
|
352
358
|
}
|
|
353
359
|
catch {
|
|
354
|
-
ctx.ui.notify(`Update failed. Try manually:
|
|
360
|
+
ctx.ui.notify(`Update failed. Try manually: ${installCmd}`, "error");
|
|
355
361
|
}
|
|
356
362
|
}
|
|
@@ -14,6 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
import { gsdRoot } from "./paths.js";
|
|
15
15
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
16
16
|
import { effectiveLockFile } from "./session-lock.js";
|
|
17
|
+
import { emitJournalEvent, queryJournal } from "./journal.js";
|
|
17
18
|
function lockPath(basePath) {
|
|
18
19
|
return join(gsdRoot(basePath), effectiveLockFile());
|
|
19
20
|
}
|
|
@@ -110,3 +111,53 @@ export function formatCrashInfo(lock) {
|
|
|
110
111
|
}
|
|
111
112
|
return lines.join("\n");
|
|
112
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Emit a synthetic unit-end event for a unit that crashed without emitting its own.
|
|
116
|
+
*
|
|
117
|
+
* Queries the journal to find the most recent unit-start for the crashed unit.
|
|
118
|
+
* If a matching unit-end already exists (e.g. the hard timeout fired), this is a
|
|
119
|
+
* no-op. Called during crash recovery, before clearing the stale lock.
|
|
120
|
+
*
|
|
121
|
+
* Addresses the gap reported in #3348 where `unit-start` was emitted but no
|
|
122
|
+
* `unit-end` followed — side effects landed but the worker died before closeout.
|
|
123
|
+
*/
|
|
124
|
+
export function emitCrashRecoveredUnitEnd(basePath, lock) {
|
|
125
|
+
// Skip bootstrap / starting pseudo-units — they have no meaningful unit-start event.
|
|
126
|
+
if (!lock.unitType || !lock.unitId || lock.unitType === "starting")
|
|
127
|
+
return;
|
|
128
|
+
try {
|
|
129
|
+
const all = queryJournal(basePath);
|
|
130
|
+
// Find the most recent unit-start for this unitId
|
|
131
|
+
const starts = all.filter((e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId);
|
|
132
|
+
if (starts.length === 0)
|
|
133
|
+
return;
|
|
134
|
+
const lastStart = starts[starts.length - 1];
|
|
135
|
+
// Check if a unit-end was already emitted (e.g. hard timeout fired after the crash)
|
|
136
|
+
const alreadyClosed = all.some((e) => e.eventType === "unit-end" &&
|
|
137
|
+
e.data?.unitId === lock.unitId &&
|
|
138
|
+
e.causedBy?.flowId === lastStart.flowId &&
|
|
139
|
+
e.causedBy?.seq === lastStart.seq);
|
|
140
|
+
if (alreadyClosed)
|
|
141
|
+
return;
|
|
142
|
+
// Find the highest seq in this flow for monotonic ordering
|
|
143
|
+
const maxSeq = all
|
|
144
|
+
.filter((e) => e.flowId === lastStart.flowId)
|
|
145
|
+
.reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
|
|
146
|
+
emitJournalEvent(basePath, {
|
|
147
|
+
ts: new Date().toISOString(),
|
|
148
|
+
flowId: lastStart.flowId,
|
|
149
|
+
seq: maxSeq + 1,
|
|
150
|
+
eventType: "unit-end",
|
|
151
|
+
data: {
|
|
152
|
+
unitType: lock.unitType,
|
|
153
|
+
unitId: lock.unitId,
|
|
154
|
+
status: "crash-recovered",
|
|
155
|
+
artifactVerified: false,
|
|
156
|
+
},
|
|
157
|
+
causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Never throw from crash recovery path — journal failure must not block recovery
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -157,7 +157,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
157
157
|
|
|
158
158
|
- `phases`: fine-grained control over which phases run. Usually set by `token_profile`, but can be overridden. Keys:
|
|
159
159
|
- `skip_research`: boolean — skip milestone-level research. Default: `false`.
|
|
160
|
-
- `reassess_after_slice`: boolean — run roadmap reassessment after each completed slice. Default: `
|
|
160
|
+
- `reassess_after_slice`: boolean — run roadmap reassessment after each completed slice. Default: `true`.
|
|
161
161
|
- `skip_reassess`: boolean — force-disable roadmap reassessment even if `reassess_after_slice` is enabled. Default: `false`.
|
|
162
162
|
- `skip_slice_research`: boolean — skip per-slice research. Default: `false`.
|
|
163
163
|
|
|
@@ -1352,6 +1352,25 @@ export function setSliceSummaryMd(milestoneId, sliceId, summaryMd, uatMd) {
|
|
|
1352
1352
|
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
1353
1353
|
currentDb.prepare(`UPDATE slices SET full_summary_md = :summary_md, full_uat_md = :uat_md WHERE milestone_id = :mid AND id = :sid`).run({ ":mid": milestoneId, ":sid": sliceId, ":summary_md": summaryMd, ":uat_md": uatMd });
|
|
1354
1354
|
}
|
|
1355
|
+
function parseTaskArrayColumn(raw) {
|
|
1356
|
+
if (typeof raw !== "string" || raw.trim() === "")
|
|
1357
|
+
return [];
|
|
1358
|
+
try {
|
|
1359
|
+
const parsed = JSON.parse(raw);
|
|
1360
|
+
if (Array.isArray(parsed))
|
|
1361
|
+
return parsed.map((value) => String(value));
|
|
1362
|
+
if (parsed === null || parsed === undefined || parsed === "")
|
|
1363
|
+
return [];
|
|
1364
|
+
return [String(parsed)];
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
// Older/corrupt rows may contain comma-separated strings instead of JSON.
|
|
1368
|
+
return raw
|
|
1369
|
+
.split(",")
|
|
1370
|
+
.map((value) => value.trim())
|
|
1371
|
+
.filter(Boolean);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1355
1374
|
function rowToTask(row) {
|
|
1356
1375
|
const parseTaskArray = (value) => {
|
|
1357
1376
|
if (Array.isArray(value)) {
|
|
@@ -1390,8 +1409,8 @@ function rowToTask(row) {
|
|
|
1390
1409
|
blocker_discovered: row["blocker_discovered"] === 1,
|
|
1391
1410
|
deviations: row["deviations"],
|
|
1392
1411
|
known_issues: row["known_issues"],
|
|
1393
|
-
key_files:
|
|
1394
|
-
key_decisions:
|
|
1412
|
+
key_files: parseTaskArrayColumn(row["key_files"]),
|
|
1413
|
+
key_decisions: parseTaskArrayColumn(row["key_decisions"]),
|
|
1395
1414
|
full_summary_md: row["full_summary_md"],
|
|
1396
1415
|
description: row["description"] ?? "",
|
|
1397
1416
|
estimate: row["estimate"] ?? "",
|
|
@@ -1855,6 +1874,21 @@ export function deleteSlice(milestoneId, sliceId) {
|
|
|
1855
1874
|
currentDb.prepare(`DELETE FROM slices WHERE milestone_id = :mid AND id = :sid`).run({ ":mid": milestoneId, ":sid": sliceId });
|
|
1856
1875
|
});
|
|
1857
1876
|
}
|
|
1877
|
+
export function deleteMilestone(milestoneId) {
|
|
1878
|
+
if (!currentDb)
|
|
1879
|
+
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
1880
|
+
transaction(() => {
|
|
1881
|
+
currentDb.prepare(`DELETE FROM verification_evidence WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
1882
|
+
currentDb.prepare(`DELETE FROM quality_gates WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
1883
|
+
currentDb.prepare(`DELETE FROM tasks WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
1884
|
+
currentDb.prepare(`DELETE FROM slice_dependencies WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
1885
|
+
currentDb.prepare(`DELETE FROM slices WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
1886
|
+
currentDb.prepare(`DELETE FROM replan_history WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
1887
|
+
currentDb.prepare(`DELETE FROM assessments WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
1888
|
+
currentDb.prepare(`DELETE FROM artifacts WHERE milestone_id = :mid`).run({ ":mid": milestoneId });
|
|
1889
|
+
currentDb.prepare(`DELETE FROM milestones WHERE id = :mid`).run({ ":mid": milestoneId });
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1858
1892
|
export function updateSliceFields(milestoneId, sliceId, fields) {
|
|
1859
1893
|
if (!currentDb)
|
|
1860
1894
|
throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
@@ -15,7 +15,8 @@ import { join } from "node:path";
|
|
|
15
15
|
import { resolveMilestonePath, resolveMilestoneFile, buildMilestoneFileName, } from "./paths.js";
|
|
16
16
|
import { invalidateAllCaches } from "./cache.js";
|
|
17
17
|
import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
|
|
18
|
-
import { getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
18
|
+
import { deleteMilestone, getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
19
|
+
import { removeWorktree } from "./worktree-manager.js";
|
|
19
20
|
import { logWarning } from "./workflow-logger.js";
|
|
20
21
|
// ─── Park ──────────────────────────────────────────────────────────────────
|
|
21
22
|
/**
|
|
@@ -99,12 +100,29 @@ export function discardMilestone(basePath, milestoneId) {
|
|
|
99
100
|
const mDir = resolveMilestonePath(basePath, milestoneId);
|
|
100
101
|
if (!mDir || !existsSync(mDir))
|
|
101
102
|
return false;
|
|
103
|
+
try {
|
|
104
|
+
removeWorktree(basePath, milestoneId, {
|
|
105
|
+
branch: `milestone/${milestoneId}`,
|
|
106
|
+
deleteBranch: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
logWarning("engine", `discardMilestone worktree cleanup failed for ${milestoneId}: ${err.message}`);
|
|
111
|
+
}
|
|
102
112
|
rmSync(mDir, { recursive: true, force: true });
|
|
103
113
|
// Prune from queue order if present
|
|
104
114
|
const order = loadQueueOrder(basePath);
|
|
105
115
|
if (order && order.includes(milestoneId)) {
|
|
106
116
|
saveQueueOrder(basePath, order.filter(id => id !== milestoneId));
|
|
107
117
|
}
|
|
118
|
+
if (isDbAvailable()) {
|
|
119
|
+
try {
|
|
120
|
+
deleteMilestone(milestoneId);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
logWarning("engine", `discardMilestone DB cleanup failed for ${milestoneId}: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
108
126
|
invalidateAllCaches();
|
|
109
127
|
return true;
|
|
110
128
|
}
|