gsd-pi 2.18.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/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +45 -15
- package/dist/resources/extensions/gsd/auto.ts +276 -19
- package/dist/resources/extensions/gsd/captures.ts +384 -0
- package/dist/resources/extensions/gsd/commands.ts +139 -3
- 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/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/post-unit-hooks.ts +2 -1
- package/dist/resources/extensions/gsd/preferences.ts +73 -0
- package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
- 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/triage-captures.md +62 -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/feature-branch-lifecycle-integration.test.ts +434 -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/remote-questions.test.ts +227 -1
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- 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/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/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +45 -15
- package/src/resources/extensions/gsd/auto.ts +276 -19
- package/src/resources/extensions/gsd/captures.ts +384 -0
- package/src/resources/extensions/gsd/commands.ts +139 -3
- 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/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/post-unit-hooks.ts +2 -1
- package/src/resources/extensions/gsd/preferences.ts +73 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
- 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/triage-captures.md +62 -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/feature-branch-lifecycle-integration.test.ts +434 -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/remote-questions.test.ts +227 -1
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
- 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/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
|
@@ -10,7 +10,7 @@ import type { ExtensionContext, ExtensionCommandContext } from "@gsd/pi-coding-a
|
|
|
10
10
|
import type { GSDState } from "./types.js";
|
|
11
11
|
import { getCurrentBranch } from "./worktree.js";
|
|
12
12
|
import { getActiveHook } from "./post-unit-hooks.js";
|
|
13
|
-
import { getLedger, getProjectTotals, formatCost, formatTokenCount } from "./metrics.js";
|
|
13
|
+
import { getLedger, getProjectTotals, formatCost, formatTokenCount, formatTierSavings } from "./metrics.js";
|
|
14
14
|
import {
|
|
15
15
|
resolveMilestoneFile,
|
|
16
16
|
resolveSliceFile,
|
|
@@ -39,6 +39,8 @@ export interface AutoDashboardData {
|
|
|
39
39
|
projectedRemainingCost?: number;
|
|
40
40
|
/** Whether token profile has been auto-downgraded due to budget prediction */
|
|
41
41
|
profileDowngraded?: boolean;
|
|
42
|
+
/** Number of pending captures awaiting triage (0 if none or file missing) */
|
|
43
|
+
pendingCaptureCount: number;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// ─── Unit Description Helpers ─────────────────────────────────────────────────
|
|
@@ -239,6 +241,7 @@ export function updateProgressWidget(
|
|
|
239
241
|
unitId: string,
|
|
240
242
|
state: GSDState,
|
|
241
243
|
accessors: WidgetStateAccessors,
|
|
244
|
+
tierBadge?: string,
|
|
242
245
|
): void {
|
|
243
246
|
if (!ctx.hasUI) return;
|
|
244
247
|
|
|
@@ -319,7 +322,8 @@ export function updateProgressWidget(
|
|
|
319
322
|
|
|
320
323
|
const target = task ? `${task.id}: ${task.title}` : unitId;
|
|
321
324
|
const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
|
|
322
|
-
const
|
|
325
|
+
const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
|
|
326
|
+
const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
|
|
323
327
|
lines.push(rightAlign(actionLeft, phaseBadge, width));
|
|
324
328
|
lines.push("");
|
|
325
329
|
|
|
@@ -414,6 +418,14 @@ export function updateProgressWidget(
|
|
|
414
418
|
? `${modelPhase}${theme.fg("dim", modelDisplay)}`
|
|
415
419
|
: "";
|
|
416
420
|
lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
|
|
421
|
+
|
|
422
|
+
// Dynamic routing savings summary
|
|
423
|
+
if (mLedger && mLedger.units.some(u => u.tier)) {
|
|
424
|
+
const savings = formatTierSavings(mLedger.units);
|
|
425
|
+
if (savings) {
|
|
426
|
+
lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
417
429
|
}
|
|
418
430
|
|
|
419
431
|
const hintParts: string[] = [];
|
|
@@ -389,7 +389,7 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
|
|
|
389
389
|
milestoneId: mid, milestoneTitle: midTitle,
|
|
390
390
|
milestonePath: relMilestonePath(base, mid),
|
|
391
391
|
contextPath: contextRel,
|
|
392
|
-
outputPath: outputRelPath,
|
|
392
|
+
outputPath: join(base, outputRelPath),
|
|
393
393
|
inlinedContext,
|
|
394
394
|
...buildSkillDiscoveryVars(),
|
|
395
395
|
});
|
|
@@ -432,14 +432,14 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
|
|
|
432
432
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
433
433
|
|
|
434
434
|
const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
|
|
435
|
-
const secretsOutputPath = relMilestoneFile(base, mid, "SECRETS");
|
|
435
|
+
const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
|
|
436
436
|
return loadPrompt("plan-milestone", {
|
|
437
437
|
workingDirectory: base,
|
|
438
438
|
milestoneId: mid, milestoneTitle: midTitle,
|
|
439
439
|
milestonePath: relMilestonePath(base, mid),
|
|
440
440
|
contextPath: contextRel,
|
|
441
441
|
researchPath: researchRel,
|
|
442
|
-
outputPath: outputRelPath,
|
|
442
|
+
outputPath: join(base, outputRelPath),
|
|
443
443
|
secretsOutputPath,
|
|
444
444
|
inlinedContext,
|
|
445
445
|
});
|
|
@@ -484,7 +484,7 @@ export async function buildResearchSlicePrompt(
|
|
|
484
484
|
roadmapPath: roadmapRel,
|
|
485
485
|
contextPath: contextRel,
|
|
486
486
|
milestoneResearchPath: milestoneResearchRel,
|
|
487
|
-
outputPath: outputRelPath,
|
|
487
|
+
outputPath: join(base, outputRelPath),
|
|
488
488
|
inlinedContext,
|
|
489
489
|
dependencySummaries: depContent,
|
|
490
490
|
...buildSkillDiscoveryVars(),
|
|
@@ -531,7 +531,7 @@ export async function buildPlanSlicePrompt(
|
|
|
531
531
|
slicePath: relSlicePath(base, mid, sid),
|
|
532
532
|
roadmapPath: roadmapRel,
|
|
533
533
|
researchPath: researchRel,
|
|
534
|
-
outputPath: outputRelPath,
|
|
534
|
+
outputPath: join(base, outputRelPath),
|
|
535
535
|
inlinedContext,
|
|
536
536
|
dependencySummaries: depContent,
|
|
537
537
|
});
|
|
@@ -598,7 +598,7 @@ export async function buildExecuteTaskPrompt(
|
|
|
598
598
|
...(knowledgeInlineET ? [knowledgeInlineET] : []),
|
|
599
599
|
].join("\n\n---\n\n");
|
|
600
600
|
|
|
601
|
-
const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md
|
|
601
|
+
const taskSummaryPath = join(base, `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`);
|
|
602
602
|
|
|
603
603
|
const activeOverrides = await loadActiveOverrides(base);
|
|
604
604
|
const overridesSection = formatOverridesSection(activeOverrides);
|
|
@@ -607,7 +607,7 @@ export async function buildExecuteTaskPrompt(
|
|
|
607
607
|
overridesSection,
|
|
608
608
|
workingDirectory: base,
|
|
609
609
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
|
|
610
|
-
planPath: relSliceFile(base, mid, sid, "PLAN"),
|
|
610
|
+
planPath: join(base, relSliceFile(base, mid, sid, "PLAN")),
|
|
611
611
|
slicePath: relSlicePath(base, mid, sid),
|
|
612
612
|
taskPlanPath: taskPlanRelPath,
|
|
613
613
|
taskPlanInline,
|
|
@@ -665,14 +665,14 @@ export async function buildCompleteSlicePrompt(
|
|
|
665
665
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
666
666
|
|
|
667
667
|
const sliceRel = relSlicePath(base, mid, sid);
|
|
668
|
-
const sliceSummaryPath = `${sliceRel}/${sid}-SUMMARY.md
|
|
669
|
-
const sliceUatPath = `${sliceRel}/${sid}-UAT.md
|
|
668
|
+
const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
|
|
669
|
+
const sliceUatPath = join(base, `${sliceRel}/${sid}-UAT.md`);
|
|
670
670
|
|
|
671
671
|
return loadPrompt("complete-slice", {
|
|
672
672
|
workingDirectory: base,
|
|
673
673
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
|
|
674
674
|
slicePath: sliceRel,
|
|
675
|
-
roadmapPath: roadmapRel,
|
|
675
|
+
roadmapPath: join(base, roadmapRel),
|
|
676
676
|
inlinedContext,
|
|
677
677
|
sliceSummaryPath,
|
|
678
678
|
sliceUatPath,
|
|
@@ -723,7 +723,7 @@ export async function buildCompleteMilestonePrompt(
|
|
|
723
723
|
|
|
724
724
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
725
725
|
|
|
726
|
-
const milestoneSummaryPath = `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md
|
|
726
|
+
const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
|
|
727
727
|
|
|
728
728
|
return loadPrompt("complete-milestone", {
|
|
729
729
|
workingDirectory: base,
|
|
@@ -775,7 +775,21 @@ export async function buildReplanSlicePrompt(
|
|
|
775
775
|
|
|
776
776
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
777
777
|
|
|
778
|
-
const replanPath = `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md
|
|
778
|
+
const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
|
|
779
|
+
|
|
780
|
+
// Build capture context for replan prompt (captures that triggered this replan)
|
|
781
|
+
let captureContext = "(none)";
|
|
782
|
+
try {
|
|
783
|
+
const { loadReplanCaptures } = await import("./triage-resolution.js");
|
|
784
|
+
const replanCaptures = loadReplanCaptures(base);
|
|
785
|
+
if (replanCaptures.length > 0) {
|
|
786
|
+
captureContext = replanCaptures.map(c =>
|
|
787
|
+
`- **${c.id}**: "${c.text}" — ${c.rationale ?? "no rationale"}`
|
|
788
|
+
).join("\n");
|
|
789
|
+
}
|
|
790
|
+
} catch {
|
|
791
|
+
// Non-fatal — captures module may not be available
|
|
792
|
+
}
|
|
779
793
|
|
|
780
794
|
return loadPrompt("replan-slice", {
|
|
781
795
|
workingDirectory: base,
|
|
@@ -783,10 +797,11 @@ export async function buildReplanSlicePrompt(
|
|
|
783
797
|
sliceId: sid,
|
|
784
798
|
sliceTitle: sTitle,
|
|
785
799
|
slicePath: relSlicePath(base, mid, sid),
|
|
786
|
-
planPath: slicePlanRel,
|
|
800
|
+
planPath: join(base, slicePlanRel),
|
|
787
801
|
blockerTaskId,
|
|
788
802
|
inlinedContext,
|
|
789
803
|
replanPath,
|
|
804
|
+
captureContext,
|
|
790
805
|
});
|
|
791
806
|
}
|
|
792
807
|
|
|
@@ -808,7 +823,7 @@ export async function buildRunUatPrompt(
|
|
|
808
823
|
|
|
809
824
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
810
825
|
|
|
811
|
-
const uatResultPath = relSliceFile(base, mid, sliceId, "UAT-RESULT");
|
|
826
|
+
const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
|
|
812
827
|
const uatType = extractUatType(uatContent) ?? "human-experience";
|
|
813
828
|
|
|
814
829
|
return loadPrompt("run-uat", {
|
|
@@ -847,7 +862,21 @@ export async function buildReassessRoadmapPrompt(
|
|
|
847
862
|
|
|
848
863
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
849
864
|
|
|
850
|
-
const assessmentPath = relSliceFile(base, mid, completedSliceId, "ASSESSMENT");
|
|
865
|
+
const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
|
|
866
|
+
|
|
867
|
+
// Build deferred captures context for reassess prompt
|
|
868
|
+
let deferredCaptures = "(none)";
|
|
869
|
+
try {
|
|
870
|
+
const { loadDeferredCaptures } = await import("./triage-resolution.js");
|
|
871
|
+
const deferred = loadDeferredCaptures(base);
|
|
872
|
+
if (deferred.length > 0) {
|
|
873
|
+
deferredCaptures = deferred.map(c =>
|
|
874
|
+
`- **${c.id}**: "${c.text}" — ${c.rationale ?? "deferred during triage"}`
|
|
875
|
+
).join("\n");
|
|
876
|
+
}
|
|
877
|
+
} catch {
|
|
878
|
+
// Non-fatal — captures module may not be available
|
|
879
|
+
}
|
|
851
880
|
|
|
852
881
|
return loadPrompt("reassess-roadmap", {
|
|
853
882
|
workingDirectory: base,
|
|
@@ -858,6 +887,7 @@ export async function buildReassessRoadmapPrompt(
|
|
|
858
887
|
completedSliceSummaryPath: summaryRel,
|
|
859
888
|
assessmentPath,
|
|
860
889
|
inlinedContext,
|
|
890
|
+
deferredCaptures,
|
|
861
891
|
});
|
|
862
892
|
}
|
|
863
893
|
|
|
@@ -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 {
|
|
@@ -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,
|
|
@@ -129,6 +133,7 @@ import {
|
|
|
129
133
|
deregisterSigtermHandler as _deregisterSigtermHandler,
|
|
130
134
|
detectWorkingTreeActivity,
|
|
131
135
|
} from "./auto-supervisor.js";
|
|
136
|
+
import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from "./captures.js";
|
|
132
137
|
|
|
133
138
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
134
139
|
|
|
@@ -233,6 +238,9 @@ let autoStartTime: number = 0;
|
|
|
233
238
|
let completedUnits: { type: string; id: string; startedAt: number; finishedAt: number }[] = [];
|
|
234
239
|
let currentUnit: { type: string; id: string; startedAt: number } | null = null;
|
|
235
240
|
|
|
241
|
+
/** Track dynamic routing decision for the current unit (for metrics) */
|
|
242
|
+
let currentUnitRouting: { tier: string; modelDowngraded: boolean } | null = null;
|
|
243
|
+
|
|
236
244
|
/** Track current milestone to detect transitions */
|
|
237
245
|
let currentMilestoneId: string | null = null;
|
|
238
246
|
let lastBudgetAlertLevel: BudgetAlertLevel = 0;
|
|
@@ -301,6 +309,15 @@ export { type AutoDashboardData } from "./auto-dashboard.js";
|
|
|
301
309
|
export function getAutoDashboardData(): AutoDashboardData {
|
|
302
310
|
const ledger = getLedger();
|
|
303
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
|
+
}
|
|
304
321
|
return {
|
|
305
322
|
active,
|
|
306
323
|
paused,
|
|
@@ -312,6 +329,7 @@ export function getAutoDashboardData(): AutoDashboardData {
|
|
|
312
329
|
basePath,
|
|
313
330
|
totalCost: totals?.cost ?? 0,
|
|
314
331
|
totalTokens: totals?.tokens.total ?? 0,
|
|
332
|
+
pendingCaptureCount,
|
|
315
333
|
};
|
|
316
334
|
}
|
|
317
335
|
|
|
@@ -504,6 +522,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
504
522
|
}
|
|
505
523
|
|
|
506
524
|
resetMetrics();
|
|
525
|
+
resetRoutingHistory();
|
|
507
526
|
resetHookState();
|
|
508
527
|
if (basePath) clearPersistedHookState(basePath);
|
|
509
528
|
active = false;
|
|
@@ -809,6 +828,9 @@ export async function startAuto(
|
|
|
809
828
|
// Initialize metrics — loads existing ledger from disk
|
|
810
829
|
initMetrics(base);
|
|
811
830
|
|
|
831
|
+
// Initialize routing history for adaptive learning
|
|
832
|
+
initRoutingHistory(base);
|
|
833
|
+
|
|
812
834
|
// Snapshot installed skills so we can detect new ones after research
|
|
813
835
|
if (resolveSkillDiscoveryMode() !== "off") {
|
|
814
836
|
snapshotSkills();
|
|
@@ -1011,7 +1033,7 @@ export async function handleAgentEnd(
|
|
|
1011
1033
|
const hookStartedAt = Date.now();
|
|
1012
1034
|
if (currentUnit) {
|
|
1013
1035
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1014
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1036
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1015
1037
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1016
1038
|
}
|
|
1017
1039
|
currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
|
|
@@ -1106,6 +1128,108 @@ export async function handleAgentEnd(
|
|
|
1106
1128
|
}
|
|
1107
1129
|
}
|
|
1108
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
|
+
|
|
1109
1233
|
// In step mode, pause and show a wizard instead of immediately dispatching
|
|
1110
1234
|
if (stepMode) {
|
|
1111
1235
|
await showStepWizard(ctx, pi);
|
|
@@ -1227,7 +1351,10 @@ function updateProgressWidget(
|
|
|
1227
1351
|
unitId: string,
|
|
1228
1352
|
state: GSDState,
|
|
1229
1353
|
): void {
|
|
1230
|
-
|
|
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);
|
|
1231
1358
|
}
|
|
1232
1359
|
|
|
1233
1360
|
/** State accessors for the widget — closures over module globals. */
|
|
@@ -1306,12 +1433,85 @@ async function dispatchNextUnit(
|
|
|
1306
1433
|
"info",
|
|
1307
1434
|
);
|
|
1308
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
|
+
}
|
|
1309
1441
|
// Reset stuck detection for new milestone
|
|
1310
1442
|
unitDispatchCount.clear();
|
|
1311
1443
|
unitRecoveryCount.clear();
|
|
1312
1444
|
unitLifetimeDispatches.clear();
|
|
1313
|
-
//
|
|
1314
|
-
|
|
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
|
+
|
|
1315
1515
|
// Prune completed milestone from queue order file
|
|
1316
1516
|
const pendingIds = state.registry
|
|
1317
1517
|
.filter(m => m.status !== "complete")
|
|
@@ -1327,7 +1527,7 @@ async function dispatchNextUnit(
|
|
|
1327
1527
|
// Save final session before stopping
|
|
1328
1528
|
if (currentUnit) {
|
|
1329
1529
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1330
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1530
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1331
1531
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1332
1532
|
}
|
|
1333
1533
|
sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
@@ -1355,7 +1555,7 @@ async function dispatchNextUnit(
|
|
|
1355
1555
|
if (!mid || !midTitle) {
|
|
1356
1556
|
if (currentUnit) {
|
|
1357
1557
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1358
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1558
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1359
1559
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1360
1560
|
}
|
|
1361
1561
|
await stopAuto(ctx, pi);
|
|
@@ -1370,7 +1570,7 @@ async function dispatchNextUnit(
|
|
|
1370
1570
|
if (state.phase === "complete") {
|
|
1371
1571
|
if (currentUnit) {
|
|
1372
1572
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1373
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1573
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1374
1574
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1375
1575
|
}
|
|
1376
1576
|
// Clear completed-units.json for the finished milestone so it doesn't grow unbounded.
|
|
@@ -1440,7 +1640,7 @@ async function dispatchNextUnit(
|
|
|
1440
1640
|
if (state.phase === "blocked") {
|
|
1441
1641
|
if (currentUnit) {
|
|
1442
1642
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1443
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1643
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1444
1644
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1445
1645
|
}
|
|
1446
1646
|
await stopAuto(ctx, pi);
|
|
@@ -1548,7 +1748,7 @@ async function dispatchNextUnit(
|
|
|
1548
1748
|
if (dispatchResult.action === "stop") {
|
|
1549
1749
|
if (currentUnit) {
|
|
1550
1750
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1551
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1751
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1552
1752
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1553
1753
|
}
|
|
1554
1754
|
await stopAuto(ctx, pi);
|
|
@@ -1658,7 +1858,7 @@ async function dispatchNextUnit(
|
|
|
1658
1858
|
if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
|
|
1659
1859
|
if (currentUnit) {
|
|
1660
1860
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1661
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1861
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1662
1862
|
}
|
|
1663
1863
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
1664
1864
|
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
@@ -1672,7 +1872,7 @@ async function dispatchNextUnit(
|
|
|
1672
1872
|
if (prevCount >= MAX_UNIT_DISPATCHES) {
|
|
1673
1873
|
if (currentUnit) {
|
|
1674
1874
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1675
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1875
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1676
1876
|
}
|
|
1677
1877
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
1678
1878
|
|
|
@@ -1830,9 +2030,19 @@ async function dispatchNextUnit(
|
|
|
1830
2030
|
// The session still holds the previous unit's data (newSession hasn't fired yet).
|
|
1831
2031
|
if (currentUnit) {
|
|
1832
2032
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1833
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2033
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
1834
2034
|
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1835
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
|
+
|
|
1836
2046
|
// Only mark the previous unit as completed if:
|
|
1837
2047
|
// 1. We're not about to re-dispatch the same unit (retry scenario)
|
|
1838
2048
|
// 2. The expected artifact actually exists on disk
|
|
@@ -1935,7 +2145,54 @@ async function dispatchNextUnit(
|
|
|
1935
2145
|
const modelConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
1936
2146
|
if (modelConfig) {
|
|
1937
2147
|
const availableModels = ctx.modelRegistry.getAvailable();
|
|
1938
|
-
|
|
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];
|
|
1939
2196
|
let modelSet = false;
|
|
1940
2197
|
|
|
1941
2198
|
for (const modelId of modelsToTry) {
|
|
@@ -2000,11 +2257,11 @@ async function dispatchNextUnit(
|
|
|
2000
2257
|
|
|
2001
2258
|
const ok = await pi.setModel(model, { persist: false });
|
|
2002
2259
|
if (ok) {
|
|
2003
|
-
const fallbackNote = modelId ===
|
|
2260
|
+
const fallbackNote = modelId === effectiveModelConfig.primary
|
|
2004
2261
|
? ""
|
|
2005
|
-
: ` (fallback from ${
|
|
2262
|
+
: ` (fallback from ${effectiveModelConfig.primary})`;
|
|
2006
2263
|
const phase = unitPhaseLabel(unitType);
|
|
2007
|
-
ctx.ui.notify(`Model [${phase}]: ${model.provider}/${model.id}${fallbackNote}`, "info");
|
|
2264
|
+
ctx.ui.notify(`Model [${phase}]${routingTierLabel}: ${model.provider}/${model.id}${fallbackNote}`, "info");
|
|
2008
2265
|
modelSet = true;
|
|
2009
2266
|
break;
|
|
2010
2267
|
} else {
|
|
@@ -2083,7 +2340,7 @@ async function dispatchNextUnit(
|
|
|
2083
2340
|
|
|
2084
2341
|
if (currentUnit) {
|
|
2085
2342
|
const modelId = ctx.model?.id ?? "unknown";
|
|
2086
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2343
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
2087
2344
|
}
|
|
2088
2345
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2089
2346
|
|
|
@@ -2109,7 +2366,7 @@ async function dispatchNextUnit(
|
|
|
2109
2366
|
timeoutAt: Date.now(),
|
|
2110
2367
|
});
|
|
2111
2368
|
const modelId = ctx.model?.id ?? "unknown";
|
|
2112
|
-
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2369
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
|
|
2113
2370
|
}
|
|
2114
2371
|
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2115
2372
|
|