gsd-pi 2.17.0 → 2.19.0
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/README.md +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +399 -29
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +382 -23
- package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/dist/resources/extensions/gsd/files.ts +123 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/metrics.ts +48 -0
- package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/dist/resources/extensions/gsd/model-router.ts +256 -0
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +132 -1
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +22 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/dist/resources/extensions/remote-questions/format.ts +12 -6
- package/dist/resources/extensions/remote-questions/manager.ts +8 -0
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- 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 +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +399 -29
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +382 -23
- package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
- package/src/resources/extensions/gsd/files.ts +123 -1
- package/src/resources/extensions/gsd/guided-flow.ts +237 -4
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/metrics.ts +48 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
- package/src/resources/extensions/gsd/model-router.ts +256 -0
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +132 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
- package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
- package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
- package/src/resources/extensions/gsd/triage-ui.ts +175 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
- package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
- package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +22 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
- package/src/resources/extensions/remote-questions/format.ts +12 -6
- package/src/resources/extensions/remote-questions/manager.ts +8 -0
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
import { deriveState, invalidateStateCache } from "./state.js";
|
|
20
20
|
import type { BudgetEnforcementMode, GSDState } from "./types.js";
|
|
21
21
|
import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides } from "./files.js";
|
|
22
|
+
import { loadPrompt } from "./prompt-loader.js";
|
|
22
23
|
export { inlinePriorMilestoneSummary } from "./files.js";
|
|
23
24
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
24
25
|
import {
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
buildMilestoneFileName, buildSliceFileName, buildTaskFileName,
|
|
30
31
|
} from "./paths.js";
|
|
31
32
|
import { invalidateAllCaches } from "./cache.js";
|
|
32
|
-
import { saveActivityLog } from "./activity-log.js";
|
|
33
|
+
import { saveActivityLog, clearActivityLogState } from "./activity-log.js";
|
|
33
34
|
import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
|
|
34
35
|
import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
|
|
35
36
|
import {
|
|
@@ -39,9 +40,12 @@ import {
|
|
|
39
40
|
readUnitRuntimeRecord,
|
|
40
41
|
writeUnitRuntimeRecord,
|
|
41
42
|
} from "./unit-runtime.js";
|
|
42
|
-
import { resolveAutoSupervisorConfig, resolveModelWithFallbacksForUnit, loadEffectiveGSDPreferences, resolveSkillDiscoveryMode } from "./preferences.js";
|
|
43
|
+
import { resolveAutoSupervisorConfig, resolveModelWithFallbacksForUnit, loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, resolveDynamicRoutingConfig } from "./preferences.js";
|
|
43
44
|
import { sendDesktopNotification } from "./notifications.js";
|
|
44
45
|
import type { GSDPreferences } from "./preferences.js";
|
|
46
|
+
import { classifyUnitComplexity, tierLabel } from "./complexity-classifier.js";
|
|
47
|
+
import { resolveModelForComplexity } from "./model-router.js";
|
|
48
|
+
import { initRoutingHistory, resetRoutingHistory, recordOutcome } from "./routing-history.js";
|
|
45
49
|
import {
|
|
46
50
|
checkPostUnitHooks,
|
|
47
51
|
getActiveHook,
|
|
@@ -92,7 +96,9 @@ import {
|
|
|
92
96
|
getAutoWorktreePath,
|
|
93
97
|
getAutoWorktreeOriginalBase,
|
|
94
98
|
mergeMilestoneToMain,
|
|
99
|
+
autoWorktreeBranch,
|
|
95
100
|
} from "./auto-worktree.js";
|
|
101
|
+
import { pruneQueueOrder } from "./queue-order.js";
|
|
96
102
|
import { showNextAction } from "../shared/next-action-ui.js";
|
|
97
103
|
import {
|
|
98
104
|
resolveExpectedArtifactPath,
|
|
@@ -127,6 +133,7 @@ import {
|
|
|
127
133
|
deregisterSigtermHandler as _deregisterSigtermHandler,
|
|
128
134
|
detectWorkingTreeActivity,
|
|
129
135
|
} from "./auto-supervisor.js";
|
|
136
|
+
import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from "./captures.js";
|
|
130
137
|
|
|
131
138
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
132
139
|
|
|
@@ -196,6 +203,33 @@ function shouldUseWorktreeIsolation(): boolean {
|
|
|
196
203
|
return true; // default: worktree
|
|
197
204
|
}
|
|
198
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Detect and escape a stale worktree cwd (#608).
|
|
208
|
+
*
|
|
209
|
+
* After milestone completion + merge, the worktree directory is removed but
|
|
210
|
+
* the process cwd may still point inside `.gsd/worktrees/<MID>/`.
|
|
211
|
+
* When a new session starts, `process.cwd()` is passed as `base` to startAuto
|
|
212
|
+
* and all subsequent writes land in the wrong directory. This function detects
|
|
213
|
+
* that scenario and chdir back to the project root.
|
|
214
|
+
*
|
|
215
|
+
* Returns the corrected base path.
|
|
216
|
+
*/
|
|
217
|
+
function escapeStaleWorktree(base: string): string {
|
|
218
|
+
const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
|
|
219
|
+
const idx = base.indexOf(marker);
|
|
220
|
+
if (idx === -1) return base;
|
|
221
|
+
|
|
222
|
+
// base is inside .gsd/worktrees/<something> — extract the project root
|
|
223
|
+
const projectRoot = base.slice(0, idx);
|
|
224
|
+
try {
|
|
225
|
+
process.chdir(projectRoot);
|
|
226
|
+
} catch {
|
|
227
|
+
// If chdir fails, return the original — caller will handle errors downstream
|
|
228
|
+
return base;
|
|
229
|
+
}
|
|
230
|
+
return projectRoot;
|
|
231
|
+
}
|
|
232
|
+
|
|
199
233
|
/** Crash recovery prompt — set by startAuto, consumed by first dispatchNextUnit */
|
|
200
234
|
let pendingCrashRecovery: string | null = null;
|
|
201
235
|
|
|
@@ -204,6 +238,9 @@ let autoStartTime: number = 0;
|
|
|
204
238
|
let completedUnits: { type: string; id: string; startedAt: number; finishedAt: number }[] = [];
|
|
205
239
|
let currentUnit: { type: string; id: string; startedAt: number } | null = null;
|
|
206
240
|
|
|
241
|
+
/** Track dynamic routing decision for the current unit (for metrics) */
|
|
242
|
+
let currentUnitRouting: { tier: string; modelDowngraded: boolean } | null = null;
|
|
243
|
+
|
|
207
244
|
/** Track current milestone to detect transitions */
|
|
208
245
|
let currentMilestoneId: string | null = null;
|
|
209
246
|
let lastBudgetAlertLevel: BudgetAlertLevel = 0;
|
|
@@ -228,6 +265,9 @@ const DISPATCH_GAP_TIMEOUT_MS = 5_000; // 5 seconds
|
|
|
228
265
|
/** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
|
|
229
266
|
let _sigtermHandler: (() => void) | null = null;
|
|
230
267
|
|
|
268
|
+
/** Tool calls currently being executed — prevents false idle detection during long-running tools. */
|
|
269
|
+
const inFlightTools = new Set<string>();
|
|
270
|
+
|
|
231
271
|
type BudgetAlertLevel = 0 | 75 | 90 | 100;
|
|
232
272
|
|
|
233
273
|
export function getBudgetAlertLevel(budgetPct: number): BudgetAlertLevel {
|
|
@@ -269,6 +309,15 @@ export { type AutoDashboardData } from "./auto-dashboard.js";
|
|
|
269
309
|
export function getAutoDashboardData(): AutoDashboardData {
|
|
270
310
|
const ledger = getLedger();
|
|
271
311
|
const totals = ledger ? getProjectTotals(ledger.units) : null;
|
|
312
|
+
// Pending capture count — lazy check, non-fatal
|
|
313
|
+
let pendingCaptureCount = 0;
|
|
314
|
+
try {
|
|
315
|
+
if (basePath) {
|
|
316
|
+
pendingCaptureCount = countPendingCaptures(basePath);
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
// Non-fatal — captures module may not be loaded
|
|
320
|
+
}
|
|
272
321
|
return {
|
|
273
322
|
active,
|
|
274
323
|
paused,
|
|
@@ -280,6 +329,7 @@ export function getAutoDashboardData(): AutoDashboardData {
|
|
|
280
329
|
basePath,
|
|
281
330
|
totalCost: totals?.cost ?? 0,
|
|
282
331
|
totalTokens: totals?.tokens.total ?? 0,
|
|
332
|
+
pendingCaptureCount,
|
|
283
333
|
};
|
|
284
334
|
}
|
|
285
335
|
|
|
@@ -293,6 +343,22 @@ export function isAutoPaused(): boolean {
|
|
|
293
343
|
return paused;
|
|
294
344
|
}
|
|
295
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Mark a tool execution as in-flight. Called from index.ts on tool_execution_start.
|
|
348
|
+
* Prevents the idle watchdog from declaring the agent idle while tools are executing.
|
|
349
|
+
*/
|
|
350
|
+
export function markToolStart(toolCallId: string): void {
|
|
351
|
+
if (!active) return;
|
|
352
|
+
inFlightTools.add(toolCallId);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Mark a tool execution as completed. Called from index.ts on tool_execution_end.
|
|
357
|
+
*/
|
|
358
|
+
export function markToolEnd(toolCallId: string): void {
|
|
359
|
+
inFlightTools.delete(toolCallId);
|
|
360
|
+
}
|
|
361
|
+
|
|
296
362
|
/**
|
|
297
363
|
* Return the base path to use for the auto.lock file.
|
|
298
364
|
* Always uses the original project root (not the worktree) so that
|
|
@@ -345,6 +411,7 @@ function clearUnitTimeout(): void {
|
|
|
345
411
|
clearInterval(idleWatchdogHandle);
|
|
346
412
|
idleWatchdogHandle = null;
|
|
347
413
|
}
|
|
414
|
+
inFlightTools.clear();
|
|
348
415
|
clearDispatchGapWatchdog();
|
|
349
416
|
}
|
|
350
417
|
|
|
@@ -426,14 +493,18 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
426
493
|
`Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
427
494
|
"warning",
|
|
428
495
|
);
|
|
429
|
-
// Force basePath back to original even if teardown failed
|
|
430
|
-
if (originalBasePath) {
|
|
431
|
-
basePath = originalBasePath;
|
|
432
|
-
try { process.chdir(basePath); } catch { /* best-effort */ }
|
|
433
|
-
}
|
|
434
496
|
}
|
|
435
497
|
}
|
|
436
498
|
|
|
499
|
+
// Always restore cwd to project root on stop (#608).
|
|
500
|
+
// Even if isInAutoWorktree returned false (e.g., module state was already
|
|
501
|
+
// cleared by mergeMilestoneToMain), the process cwd may still be inside
|
|
502
|
+
// the worktree directory. Force it back to originalBasePath.
|
|
503
|
+
if (originalBasePath) {
|
|
504
|
+
basePath = originalBasePath;
|
|
505
|
+
try { process.chdir(basePath); } catch { /* best-effort */ }
|
|
506
|
+
}
|
|
507
|
+
|
|
437
508
|
const ledger = getLedger();
|
|
438
509
|
if (ledger && ledger.units.length > 0) {
|
|
439
510
|
const totals = getProjectTotals(ledger.units);
|
|
@@ -451,6 +522,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
451
522
|
}
|
|
452
523
|
|
|
453
524
|
resetMetrics();
|
|
525
|
+
resetRoutingHistory();
|
|
454
526
|
resetHookState();
|
|
455
527
|
if (basePath) clearPersistedHookState(basePath);
|
|
456
528
|
active = false;
|
|
@@ -458,12 +530,15 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
458
530
|
stepMode = false;
|
|
459
531
|
unitDispatchCount.clear();
|
|
460
532
|
unitRecoveryCount.clear();
|
|
533
|
+
inFlightTools.clear();
|
|
461
534
|
lastBudgetAlertLevel = 0;
|
|
462
535
|
unitLifetimeDispatches.clear();
|
|
463
536
|
currentUnit = null;
|
|
464
537
|
currentMilestoneId = null;
|
|
465
538
|
originalBasePath = "";
|
|
539
|
+
completedUnits = [];
|
|
466
540
|
clearSliceProgressCache();
|
|
541
|
+
clearActivityLogState();
|
|
467
542
|
pendingCrashRecovery = null;
|
|
468
543
|
_handlingAgentEnd = false;
|
|
469
544
|
ctx?.ui.setStatus("gsd-auto", undefined);
|
|
@@ -519,6 +594,11 @@ export async function startAuto(
|
|
|
519
594
|
): Promise<void> {
|
|
520
595
|
const requestedStepMode = options?.step ?? false;
|
|
521
596
|
|
|
597
|
+
// Escape stale worktree cwd from a previous milestone (#608).
|
|
598
|
+
// After milestone merge + worktree removal, the process cwd may still point
|
|
599
|
+
// inside .gsd/worktrees/<MID>/ — detect and chdir back to project root.
|
|
600
|
+
base = escapeStaleWorktree(base);
|
|
601
|
+
|
|
522
602
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
523
603
|
// The conversation is still intact — no need to reinitialize everything.
|
|
524
604
|
if (paused) {
|
|
@@ -569,17 +649,17 @@ export async function startAuto(
|
|
|
569
649
|
ctx.ui.setFooter(hideFooter);
|
|
570
650
|
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
571
651
|
// Restore hook state from disk in case session was interrupted
|
|
572
|
-
restoreHookState(
|
|
652
|
+
restoreHookState(basePath);
|
|
573
653
|
// Rebuild disk state before resuming — user interaction during pause may have changed files
|
|
574
|
-
try { await rebuildState(
|
|
654
|
+
try { await rebuildState(basePath); } catch { /* non-fatal */ }
|
|
575
655
|
try {
|
|
576
|
-
const report = await runGSDDoctor(
|
|
656
|
+
const report = await runGSDDoctor(basePath, { fix: true });
|
|
577
657
|
if (report.fixesApplied.length > 0) {
|
|
578
658
|
ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
|
|
579
659
|
}
|
|
580
660
|
} catch { /* non-fatal */ }
|
|
581
661
|
// Self-heal: clear stale runtime records where artifacts already exist
|
|
582
|
-
await selfHealRuntimeRecords(
|
|
662
|
+
await selfHealRuntimeRecords(basePath, ctx, completedKeySet);
|
|
583
663
|
invalidateAllCaches();
|
|
584
664
|
await dispatchNextUnit(ctx, pi);
|
|
585
665
|
return;
|
|
@@ -748,6 +828,9 @@ export async function startAuto(
|
|
|
748
828
|
// Initialize metrics — loads existing ledger from disk
|
|
749
829
|
initMetrics(base);
|
|
750
830
|
|
|
831
|
+
// Initialize routing history for adaptive learning
|
|
832
|
+
initRoutingHistory(base);
|
|
833
|
+
|
|
751
834
|
// Snapshot installed skills so we can detect new ones after research
|
|
752
835
|
if (resolveSkillDiscoveryMode() !== "off") {
|
|
753
836
|
snapshotSkills();
|
|
@@ -950,7 +1033,7 @@ export async function handleAgentEnd(
|
|
|
950
1033
|
const hookStartedAt = Date.now();
|
|
951
1034
|
if (currentUnit) {
|
|
952
1035
|
const modelId = ctx.model?.id ?? "unknown";
|
|
953
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1036
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
954
1037
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
955
1038
|
}
|
|
956
1039
|
currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
|
|
@@ -1045,6 +1128,108 @@ export async function handleAgentEnd(
|
|
|
1045
1128
|
}
|
|
1046
1129
|
}
|
|
1047
1130
|
|
|
1131
|
+
// ── Triage check: dispatch triage unit if pending captures exist ──────────
|
|
1132
|
+
// Fires after hooks complete, before normal dispatch. Follows the same
|
|
1133
|
+
// early-dispatch-and-return pattern as hooks and fix-merge.
|
|
1134
|
+
// Skip for: step mode (shows wizard instead), triage units (prevent triage-on-triage),
|
|
1135
|
+
// hook units (hooks run before triage conceptually).
|
|
1136
|
+
if (
|
|
1137
|
+
!stepMode &&
|
|
1138
|
+
currentUnit &&
|
|
1139
|
+
!currentUnit.type.startsWith("hook/") &&
|
|
1140
|
+
currentUnit.type !== "triage-captures" &&
|
|
1141
|
+
currentUnit.type !== "quick-task"
|
|
1142
|
+
) {
|
|
1143
|
+
try {
|
|
1144
|
+
if (hasPendingCaptures(basePath)) {
|
|
1145
|
+
const pending = loadPendingCaptures(basePath);
|
|
1146
|
+
if (pending.length > 0) {
|
|
1147
|
+
const state = await deriveState(basePath);
|
|
1148
|
+
const mid = state.activeMilestone?.id;
|
|
1149
|
+
const sid = state.activeSlice?.id;
|
|
1150
|
+
|
|
1151
|
+
if (mid && sid) {
|
|
1152
|
+
// Build triage prompt with current context
|
|
1153
|
+
let currentPlan = "";
|
|
1154
|
+
let roadmapContext = "";
|
|
1155
|
+
const planFile = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
1156
|
+
if (planFile) currentPlan = (await loadFile(planFile)) ?? "";
|
|
1157
|
+
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
1158
|
+
if (roadmapFile) roadmapContext = (await loadFile(roadmapFile)) ?? "";
|
|
1159
|
+
|
|
1160
|
+
const capturesList = pending.map(c =>
|
|
1161
|
+
`- **${c.id}**: "${c.text}" (captured: ${c.timestamp})`
|
|
1162
|
+
).join("\n");
|
|
1163
|
+
|
|
1164
|
+
const prompt = loadPrompt("triage-captures", {
|
|
1165
|
+
pendingCaptures: capturesList,
|
|
1166
|
+
currentPlan: currentPlan || "(no active slice plan)",
|
|
1167
|
+
roadmapContext: roadmapContext || "(no active roadmap)",
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
ctx.ui.notify(
|
|
1171
|
+
`Triaging ${pending.length} pending capture${pending.length === 1 ? "" : "s"}...`,
|
|
1172
|
+
"info",
|
|
1173
|
+
);
|
|
1174
|
+
|
|
1175
|
+
// Close out previous unit metrics
|
|
1176
|
+
if (currentUnit) {
|
|
1177
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
1178
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1179
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Dispatch triage as a new unit (early-dispatch-and-return)
|
|
1183
|
+
const triageUnitType = "triage-captures";
|
|
1184
|
+
const triageUnitId = `${mid}/${sid}/triage`;
|
|
1185
|
+
const triageStartedAt = Date.now();
|
|
1186
|
+
currentUnit = { type: triageUnitType, id: triageUnitId, startedAt: triageStartedAt };
|
|
1187
|
+
writeUnitRuntimeRecord(basePath, triageUnitType, triageUnitId, triageStartedAt, {
|
|
1188
|
+
phase: "dispatched",
|
|
1189
|
+
wrapupWarningSent: false,
|
|
1190
|
+
timeoutAt: null,
|
|
1191
|
+
lastProgressAt: triageStartedAt,
|
|
1192
|
+
progressCount: 0,
|
|
1193
|
+
lastProgressKind: "dispatch",
|
|
1194
|
+
});
|
|
1195
|
+
updateProgressWidget(ctx, triageUnitType, triageUnitId, state);
|
|
1196
|
+
|
|
1197
|
+
const result = await cmdCtx!.newSession();
|
|
1198
|
+
if (result.cancelled) {
|
|
1199
|
+
await stopAuto(ctx, pi);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1203
|
+
writeLock(basePath, triageUnitType, triageUnitId, completedUnits.length, sessionFile);
|
|
1204
|
+
|
|
1205
|
+
// Start unit timeout for triage (use same supervisor config as hooks)
|
|
1206
|
+
clearUnitTimeout();
|
|
1207
|
+
const supervisor = resolveAutoSupervisorConfig();
|
|
1208
|
+
const triageTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
|
|
1209
|
+
unitTimeoutHandle = setTimeout(async () => {
|
|
1210
|
+
unitTimeoutHandle = null;
|
|
1211
|
+
if (!active) return;
|
|
1212
|
+
ctx.ui.notify(
|
|
1213
|
+
`Triage unit exceeded timeout. Pausing auto-mode.`,
|
|
1214
|
+
"warning",
|
|
1215
|
+
);
|
|
1216
|
+
await pauseAuto(ctx, pi);
|
|
1217
|
+
}, triageTimeoutMs);
|
|
1218
|
+
|
|
1219
|
+
if (!active) return;
|
|
1220
|
+
pi.sendMessage(
|
|
1221
|
+
{ customType: "gsd-auto", content: prompt, display: verbose },
|
|
1222
|
+
{ triggerTurn: true },
|
|
1223
|
+
);
|
|
1224
|
+
return; // handleAgentEnd will fire again when triage session completes
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
} catch {
|
|
1229
|
+
// Triage check failure is non-fatal — proceed to normal dispatch
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1048
1233
|
// In step mode, pause and show a wizard instead of immediately dispatching
|
|
1049
1234
|
if (stepMode) {
|
|
1050
1235
|
await showStepWizard(ctx, pi);
|
|
@@ -1166,7 +1351,10 @@ function updateProgressWidget(
|
|
|
1166
1351
|
unitId: string,
|
|
1167
1352
|
state: GSDState,
|
|
1168
1353
|
): void {
|
|
1169
|
-
|
|
1354
|
+
const badge = currentUnitRouting?.tier
|
|
1355
|
+
? ({ light: "L", standard: "S", heavy: "H" }[currentUnitRouting.tier] ?? undefined)
|
|
1356
|
+
: undefined;
|
|
1357
|
+
_updateProgressWidget(ctx, unitType, unitId, state, widgetStateAccessors, badge);
|
|
1170
1358
|
}
|
|
1171
1359
|
|
|
1172
1360
|
/** State accessors for the widget — closures over module globals. */
|
|
@@ -1245,12 +1433,90 @@ async function dispatchNextUnit(
|
|
|
1245
1433
|
"info",
|
|
1246
1434
|
);
|
|
1247
1435
|
sendDesktopNotification("GSD", `Milestone ${currentMilestoneId} complete!`, "success", "milestone");
|
|
1436
|
+
// Hint: visualizer available after milestone transition
|
|
1437
|
+
const vizPrefs = loadEffectiveGSDPreferences()?.preferences;
|
|
1438
|
+
if (vizPrefs?.auto_visualize) {
|
|
1439
|
+
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
1440
|
+
}
|
|
1248
1441
|
// Reset stuck detection for new milestone
|
|
1249
1442
|
unitDispatchCount.clear();
|
|
1250
1443
|
unitRecoveryCount.clear();
|
|
1251
1444
|
unitLifetimeDispatches.clear();
|
|
1252
|
-
//
|
|
1253
|
-
|
|
1445
|
+
// Clear completed-units.json for the finished milestone
|
|
1446
|
+
try {
|
|
1447
|
+
const file = completedKeysPath(basePath);
|
|
1448
|
+
if (existsSync(file)) writeFileSync(file, JSON.stringify([]), "utf-8");
|
|
1449
|
+
completedKeySet.clear();
|
|
1450
|
+
} catch { /* non-fatal */ }
|
|
1451
|
+
|
|
1452
|
+
// ── Worktree lifecycle on milestone transition (#616) ──────────────
|
|
1453
|
+
// When transitioning from M_old to M_new inside a worktree, we must:
|
|
1454
|
+
// 1. Merge the completed milestone's worktree back to main
|
|
1455
|
+
// 2. Re-derive state from the project root
|
|
1456
|
+
// 3. Create a new worktree for the incoming milestone
|
|
1457
|
+
// Without this, M_new runs inside M_old's worktree on the wrong branch,
|
|
1458
|
+
// and artifact paths resolve against the wrong .gsd/ directory.
|
|
1459
|
+
if (isInAutoWorktree(basePath) && originalBasePath && shouldUseWorktreeIsolation()) {
|
|
1460
|
+
try {
|
|
1461
|
+
const roadmapPath = resolveMilestoneFile(originalBasePath, currentMilestoneId, "ROADMAP");
|
|
1462
|
+
if (roadmapPath) {
|
|
1463
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1464
|
+
const mergeResult = mergeMilestoneToMain(originalBasePath, currentMilestoneId, roadmapContent);
|
|
1465
|
+
ctx.ui.notify(
|
|
1466
|
+
`Milestone ${currentMilestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1467
|
+
"info",
|
|
1468
|
+
);
|
|
1469
|
+
} else {
|
|
1470
|
+
// No roadmap found — teardown worktree without merge
|
|
1471
|
+
teardownAutoWorktree(originalBasePath, currentMilestoneId);
|
|
1472
|
+
ctx.ui.notify(`Exited worktree for ${currentMilestoneId} (no roadmap for merge).`, "info");
|
|
1473
|
+
}
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
ctx.ui.notify(
|
|
1476
|
+
`Milestone merge failed during transition: ${err instanceof Error ? err.message : String(err)}`,
|
|
1477
|
+
"warning",
|
|
1478
|
+
);
|
|
1479
|
+
// Force cwd back to project root even if merge failed
|
|
1480
|
+
if (originalBasePath) {
|
|
1481
|
+
try { process.chdir(originalBasePath); } catch { /* best-effort */ }
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Update basePath to project root (mergeMilestoneToMain already chdir'd)
|
|
1486
|
+
basePath = originalBasePath;
|
|
1487
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1488
|
+
invalidateAllCaches();
|
|
1489
|
+
|
|
1490
|
+
// Re-derive state from project root before creating new worktree
|
|
1491
|
+
state = await deriveState(basePath);
|
|
1492
|
+
mid = state.activeMilestone?.id;
|
|
1493
|
+
midTitle = state.activeMilestone?.title;
|
|
1494
|
+
|
|
1495
|
+
// Create new worktree for the incoming milestone
|
|
1496
|
+
if (mid) {
|
|
1497
|
+
captureIntegrationBranch(basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
|
|
1498
|
+
try {
|
|
1499
|
+
const wtPath = createAutoWorktree(basePath, mid);
|
|
1500
|
+
basePath = wtPath;
|
|
1501
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1502
|
+
ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
|
|
1503
|
+
} catch (err) {
|
|
1504
|
+
ctx.ui.notify(
|
|
1505
|
+
`Auto-worktree creation for ${mid} failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`,
|
|
1506
|
+
"warning",
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
} else {
|
|
1511
|
+
// Not in worktree — just capture integration branch for the new milestone
|
|
1512
|
+
captureIntegrationBranch(originalBasePath || basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Prune completed milestone from queue order file
|
|
1516
|
+
const pendingIds = state.registry
|
|
1517
|
+
.filter(m => m.status !== "complete")
|
|
1518
|
+
.map(m => m.id);
|
|
1519
|
+
pruneQueueOrder(basePath, pendingIds);
|
|
1254
1520
|
}
|
|
1255
1521
|
if (mid) {
|
|
1256
1522
|
currentMilestoneId = mid;
|
|
@@ -1261,7 +1527,7 @@ async function dispatchNextUnit(
|
|
|
1261
1527
|
// Save final session before stopping
|
|
1262
1528
|
if (currentUnit) {
|
|
1263
1529
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1264
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1530
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1265
1531
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1266
1532
|
}
|
|
1267
1533
|
sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
@@ -1289,7 +1555,7 @@ async function dispatchNextUnit(
|
|
|
1289
1555
|
if (!mid || !midTitle) {
|
|
1290
1556
|
if (currentUnit) {
|
|
1291
1557
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1292
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1558
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1293
1559
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1294
1560
|
}
|
|
1295
1561
|
await stopAuto(ctx, pi);
|
|
@@ -1304,7 +1570,7 @@ async function dispatchNextUnit(
|
|
|
1304
1570
|
if (state.phase === "complete") {
|
|
1305
1571
|
if (currentUnit) {
|
|
1306
1572
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1307
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1573
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1308
1574
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1309
1575
|
}
|
|
1310
1576
|
// Clear completed-units.json for the finished milestone so it doesn't grow unbounded.
|
|
@@ -1331,6 +1597,39 @@ async function dispatchNextUnit(
|
|
|
1331
1597
|
`Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1332
1598
|
"warning",
|
|
1333
1599
|
);
|
|
1600
|
+
// Ensure cwd is restored even if merge failed partway through (#608).
|
|
1601
|
+
// mergeMilestoneToMain may have chdir'd but then thrown, leaving us
|
|
1602
|
+
// in an indeterminate location.
|
|
1603
|
+
if (originalBasePath) {
|
|
1604
|
+
basePath = originalBasePath;
|
|
1605
|
+
try { process.chdir(basePath); } catch { /* best-effort */ }
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
} else if (currentMilestoneId && !isInAutoWorktree(basePath)) {
|
|
1609
|
+
// Branch isolation mode (#603): no worktree, but we may be on a milestone/* branch.
|
|
1610
|
+
// Squash-merge back to the integration branch (or main) before stopping.
|
|
1611
|
+
try {
|
|
1612
|
+
const currentBranch = getCurrentBranch(basePath);
|
|
1613
|
+
const milestoneBranch = autoWorktreeBranch(currentMilestoneId);
|
|
1614
|
+
if (currentBranch === milestoneBranch) {
|
|
1615
|
+
const roadmapPath = resolveMilestoneFile(basePath, currentMilestoneId, "ROADMAP");
|
|
1616
|
+
if (roadmapPath) {
|
|
1617
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1618
|
+
// mergeMilestoneToMain handles: auto-commit, checkout integration branch,
|
|
1619
|
+
// squash merge, commit, optional push, branch deletion.
|
|
1620
|
+
const mergeResult = mergeMilestoneToMain(basePath, currentMilestoneId, roadmapContent);
|
|
1621
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1622
|
+
ctx.ui.notify(
|
|
1623
|
+
`Milestone ${currentMilestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1624
|
+
"info",
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
ctx.ui.notify(
|
|
1630
|
+
`Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
|
|
1631
|
+
"warning",
|
|
1632
|
+
);
|
|
1334
1633
|
}
|
|
1335
1634
|
}
|
|
1336
1635
|
sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
@@ -1341,7 +1640,7 @@ async function dispatchNextUnit(
|
|
|
1341
1640
|
if (state.phase === "blocked") {
|
|
1342
1641
|
if (currentUnit) {
|
|
1343
1642
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1344
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1643
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1345
1644
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1346
1645
|
}
|
|
1347
1646
|
await stopAuto(ctx, pi);
|
|
@@ -1449,7 +1748,7 @@ async function dispatchNextUnit(
|
|
|
1449
1748
|
if (dispatchResult.action === "stop") {
|
|
1450
1749
|
if (currentUnit) {
|
|
1451
1750
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1452
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1751
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1453
1752
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1454
1753
|
}
|
|
1455
1754
|
await stopAuto(ctx, pi);
|
|
@@ -1559,7 +1858,7 @@ async function dispatchNextUnit(
|
|
|
1559
1858
|
if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
|
|
1560
1859
|
if (currentUnit) {
|
|
1561
1860
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1562
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1861
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1563
1862
|
}
|
|
1564
1863
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
1565
1864
|
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
@@ -1573,7 +1872,7 @@ async function dispatchNextUnit(
|
|
|
1573
1872
|
if (prevCount >= MAX_UNIT_DISPATCHES) {
|
|
1574
1873
|
if (currentUnit) {
|
|
1575
1874
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1576
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1875
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1577
1876
|
}
|
|
1578
1877
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
1579
1878
|
|
|
@@ -1731,9 +2030,19 @@ async function dispatchNextUnit(
|
|
|
1731
2030
|
// The session still holds the previous unit's data (newSession hasn't fired yet).
|
|
1732
2031
|
if (currentUnit) {
|
|
1733
2032
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1734
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2033
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1735
2034
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1736
2035
|
|
|
2036
|
+
// Record routing outcome for adaptive learning
|
|
2037
|
+
if (currentUnitRouting) {
|
|
2038
|
+
const isRetry = currentUnit.type === unitType && currentUnit.id === unitId;
|
|
2039
|
+
recordOutcome(
|
|
2040
|
+
currentUnit.type,
|
|
2041
|
+
currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
2042
|
+
!isRetry, // success = not being retried
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
1737
2046
|
// Only mark the previous unit as completed if:
|
|
1738
2047
|
// 1. We're not about to re-dispatch the same unit (retry scenario)
|
|
1739
2048
|
// 2. The expected artifact actually exists on disk
|
|
@@ -1757,6 +2066,10 @@ async function dispatchNextUnit(
|
|
|
1757
2066
|
startedAt: currentUnit.startedAt,
|
|
1758
2067
|
finishedAt: Date.now(),
|
|
1759
2068
|
});
|
|
2069
|
+
// Cap to last 200 entries to prevent unbounded growth (#611)
|
|
2070
|
+
if (completedUnits.length > 200) {
|
|
2071
|
+
completedUnits = completedUnits.slice(-200);
|
|
2072
|
+
}
|
|
1760
2073
|
clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
|
|
1761
2074
|
unitDispatchCount.delete(`${currentUnit.type}/${currentUnit.id}`);
|
|
1762
2075
|
unitRecoveryCount.delete(`${currentUnit.type}/${currentUnit.id}`);
|
|
@@ -1832,7 +2145,54 @@ async function dispatchNextUnit(
|
|
|
1832
2145
|
const modelConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
1833
2146
|
if (modelConfig) {
|
|
1834
2147
|
const availableModels = ctx.modelRegistry.getAvailable();
|
|
1835
|
-
|
|
2148
|
+
|
|
2149
|
+
// ─── Dynamic Model Routing ─────────────────────────────────────────
|
|
2150
|
+
// If enabled, classify unit complexity and potentially downgrade to a
|
|
2151
|
+
// cheaper model. The user's configured model is the ceiling.
|
|
2152
|
+
const routingConfig = resolveDynamicRoutingConfig();
|
|
2153
|
+
let effectiveModelConfig = modelConfig;
|
|
2154
|
+
let routingTierLabel = "";
|
|
2155
|
+
currentUnitRouting = null;
|
|
2156
|
+
|
|
2157
|
+
if (routingConfig.enabled) {
|
|
2158
|
+
// Compute budget pressure if budget ceiling is set
|
|
2159
|
+
let budgetPct: number | undefined;
|
|
2160
|
+
if (routingConfig.budget_pressure !== false) {
|
|
2161
|
+
const budgetCeiling = prefs?.budget_ceiling;
|
|
2162
|
+
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
2163
|
+
const currentLedger = getLedger();
|
|
2164
|
+
const totalCost = currentLedger ? getProjectTotals(currentLedger.units).cost : 0;
|
|
2165
|
+
budgetPct = totalCost / budgetCeiling;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// Classify complexity (hook routing controlled by config.hooks)
|
|
2170
|
+
const isHook = unitType.startsWith("hook/");
|
|
2171
|
+
const shouldClassify = !isHook || routingConfig.hooks !== false;
|
|
2172
|
+
|
|
2173
|
+
if (shouldClassify) {
|
|
2174
|
+
const classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
|
|
2175
|
+
const availableModelIds = availableModels.map(m => m.id);
|
|
2176
|
+
const routing = resolveModelForComplexity(classification, modelConfig, routingConfig, availableModelIds);
|
|
2177
|
+
|
|
2178
|
+
if (routing.wasDowngraded) {
|
|
2179
|
+
effectiveModelConfig = {
|
|
2180
|
+
primary: routing.modelId,
|
|
2181
|
+
fallbacks: routing.fallbacks,
|
|
2182
|
+
};
|
|
2183
|
+
if (verbose) {
|
|
2184
|
+
ctx.ui.notify(
|
|
2185
|
+
`Dynamic routing [${tierLabel(classification.tier)}]: ${routing.modelId} (${classification.reason})`,
|
|
2186
|
+
"info",
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
routingTierLabel = ` [${tierLabel(classification.tier)}]`;
|
|
2191
|
+
currentUnitRouting = { tier: classification.tier, modelDowngraded: routing.wasDowngraded };
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
const modelsToTry = [effectiveModelConfig.primary, ...effectiveModelConfig.fallbacks];
|
|
1836
2196
|
let modelSet = false;
|
|
1837
2197
|
|
|
1838
2198
|
for (const modelId of modelsToTry) {
|
|
@@ -1897,11 +2257,11 @@ async function dispatchNextUnit(
|
|
|
1897
2257
|
|
|
1898
2258
|
const ok = await pi.setModel(model, { persist: false });
|
|
1899
2259
|
if (ok) {
|
|
1900
|
-
const fallbackNote = modelId ===
|
|
2260
|
+
const fallbackNote = modelId === effectiveModelConfig.primary
|
|
1901
2261
|
? ""
|
|
1902
|
-
: ` (fallback from ${
|
|
2262
|
+
: ` (fallback from ${effectiveModelConfig.primary})`;
|
|
1903
2263
|
const phase = unitPhaseLabel(unitType);
|
|
1904
|
-
ctx.ui.notify(`Model [${phase}]: ${model.provider}/${model.id}${fallbackNote}`, "info");
|
|
2264
|
+
ctx.ui.notify(`Model [${phase}]${routingTierLabel}: ${model.provider}/${model.id}${fallbackNote}`, "info");
|
|
1905
2265
|
modelSet = true;
|
|
1906
2266
|
break;
|
|
1907
2267
|
} else {
|
|
@@ -1957,6 +2317,16 @@ async function dispatchNextUnit(
|
|
|
1957
2317
|
if (!runtime) return;
|
|
1958
2318
|
if (Date.now() - runtime.lastProgressAt < idleTimeoutMs) return;
|
|
1959
2319
|
|
|
2320
|
+
// Agent has tool calls currently executing (await_job, long bash, etc.) —
|
|
2321
|
+
// not idle, just waiting for tool completion.
|
|
2322
|
+
if (inFlightTools.size > 0) {
|
|
2323
|
+
writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
|
|
2324
|
+
lastProgressAt: Date.now(),
|
|
2325
|
+
lastProgressKind: "tool-in-flight",
|
|
2326
|
+
});
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
1960
2330
|
// Before triggering recovery, check if the agent is actually producing
|
|
1961
2331
|
// work on disk. `git status --porcelain` is cheap and catches any
|
|
1962
2332
|
// staged/unstaged/untracked changes the agent made since lastProgressAt.
|
|
@@ -1970,7 +2340,7 @@ async function dispatchNextUnit(
|
|
|
1970
2340
|
|
|
1971
2341
|
if (currentUnit) {
|
|
1972
2342
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1973
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2343
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1974
2344
|
}
|
|
1975
2345
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
1976
2346
|
|
|
@@ -1996,7 +2366,7 @@ async function dispatchNextUnit(
|
|
|
1996
2366
|
timeoutAt: Date.now(),
|
|
1997
2367
|
});
|
|
1998
2368
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1999
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2369
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
2000
2370
|
}
|
|
2001
2371
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2002
2372
|
|