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
|
@@ -849,7 +849,7 @@ export function parseContextDependsOn(content: string | null): string[] {
|
|
|
849
849
|
const fm = parseFrontmatterMap(fmLines);
|
|
850
850
|
const raw = fm['depends_on'];
|
|
851
851
|
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
852
|
-
return (raw as string[]).map(s => String(s).
|
|
852
|
+
return (raw as string[]).map(s => String(s).trim()).filter(Boolean);
|
|
853
853
|
}
|
|
854
854
|
|
|
855
855
|
/**
|
|
@@ -951,6 +951,128 @@ export async function appendOverride(basePath: string, change: string, appliedAt
|
|
|
951
951
|
}
|
|
952
952
|
}
|
|
953
953
|
|
|
954
|
+
export async function appendKnowledge(
|
|
955
|
+
basePath: string,
|
|
956
|
+
type: "rule" | "pattern" | "lesson",
|
|
957
|
+
entry: string,
|
|
958
|
+
scope: string,
|
|
959
|
+
): Promise<void> {
|
|
960
|
+
const knowledgePath = resolveGsdRootFile(basePath, "KNOWLEDGE");
|
|
961
|
+
const existing = await loadFile(knowledgePath);
|
|
962
|
+
|
|
963
|
+
if (existing) {
|
|
964
|
+
// Find the next ID for this type
|
|
965
|
+
const prefix = type === "rule" ? "K" : type === "pattern" ? "P" : "L";
|
|
966
|
+
const idPattern = new RegExp(`^\\| ${prefix}(\\d+)`, "gm");
|
|
967
|
+
let maxId = 0;
|
|
968
|
+
let match;
|
|
969
|
+
while ((match = idPattern.exec(existing)) !== null) {
|
|
970
|
+
const num = parseInt(match[1], 10);
|
|
971
|
+
if (num > maxId) maxId = num;
|
|
972
|
+
}
|
|
973
|
+
const nextId = `${prefix}${String(maxId + 1).padStart(3, "0")}`;
|
|
974
|
+
|
|
975
|
+
// Build the table row
|
|
976
|
+
let row: string;
|
|
977
|
+
if (type === "rule") {
|
|
978
|
+
row = `| ${nextId} | ${scope} | ${entry} | — | manual |`;
|
|
979
|
+
} else if (type === "pattern") {
|
|
980
|
+
row = `| ${nextId} | ${entry} | — | ${scope} |`;
|
|
981
|
+
} else {
|
|
982
|
+
row = `| ${nextId} | ${entry} | — | — | ${scope} |`;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Find the right section and append after the table header
|
|
986
|
+
const sectionHeading = type === "rule" ? "## Rules" : type === "pattern" ? "## Patterns" : "## Lessons Learned";
|
|
987
|
+
const sectionIdx = existing.indexOf(sectionHeading);
|
|
988
|
+
if (sectionIdx !== -1) {
|
|
989
|
+
// Find the end of the table header row (the |---|...| line)
|
|
990
|
+
const afterHeading = existing.indexOf("\n", sectionIdx);
|
|
991
|
+
// Find the next section or end
|
|
992
|
+
const nextSection = existing.indexOf("\n## ", afterHeading + 1);
|
|
993
|
+
const insertPoint = nextSection !== -1 ? nextSection : existing.length;
|
|
994
|
+
|
|
995
|
+
// Insert row before the next section (or at end)
|
|
996
|
+
const before = existing.slice(0, insertPoint).trimEnd();
|
|
997
|
+
const after = existing.slice(insertPoint);
|
|
998
|
+
await saveFile(knowledgePath, before + "\n" + row + "\n" + after);
|
|
999
|
+
} else {
|
|
1000
|
+
// Section not found — append at end
|
|
1001
|
+
await saveFile(knowledgePath, existing.trimEnd() + "\n\n" + row + "\n");
|
|
1002
|
+
}
|
|
1003
|
+
} else {
|
|
1004
|
+
// Create file from scratch with template header
|
|
1005
|
+
const header = [
|
|
1006
|
+
"# Project Knowledge",
|
|
1007
|
+
"",
|
|
1008
|
+
"Append-only register of project-specific rules, patterns, and lessons learned.",
|
|
1009
|
+
"Agents read this before every unit. Add entries when you discover something worth remembering.",
|
|
1010
|
+
"",
|
|
1011
|
+
].join("\n");
|
|
1012
|
+
|
|
1013
|
+
let content: string;
|
|
1014
|
+
if (type === "rule") {
|
|
1015
|
+
content = header + [
|
|
1016
|
+
"## Rules",
|
|
1017
|
+
"",
|
|
1018
|
+
"| # | Scope | Rule | Why | Added |",
|
|
1019
|
+
"|---|-------|------|-----|-------|",
|
|
1020
|
+
`| K001 | ${scope} | ${entry} | — | manual |`,
|
|
1021
|
+
"",
|
|
1022
|
+
"## Patterns",
|
|
1023
|
+
"",
|
|
1024
|
+
"| # | Pattern | Where | Notes |",
|
|
1025
|
+
"|---|---------|-------|-------|",
|
|
1026
|
+
"",
|
|
1027
|
+
"## Lessons Learned",
|
|
1028
|
+
"",
|
|
1029
|
+
"| # | What Happened | Root Cause | Fix | Scope |",
|
|
1030
|
+
"|---|--------------|------------|-----|-------|",
|
|
1031
|
+
"",
|
|
1032
|
+
].join("\n");
|
|
1033
|
+
} else if (type === "pattern") {
|
|
1034
|
+
content = header + [
|
|
1035
|
+
"## Rules",
|
|
1036
|
+
"",
|
|
1037
|
+
"| # | Scope | Rule | Why | Added |",
|
|
1038
|
+
"|---|-------|------|-----|-------|",
|
|
1039
|
+
"",
|
|
1040
|
+
"## Patterns",
|
|
1041
|
+
"",
|
|
1042
|
+
"| # | Pattern | Where | Notes |",
|
|
1043
|
+
"|---|---------|-------|-------|",
|
|
1044
|
+
`| P001 | ${entry} | — | ${scope} |`,
|
|
1045
|
+
"",
|
|
1046
|
+
"## Lessons Learned",
|
|
1047
|
+
"",
|
|
1048
|
+
"| # | What Happened | Root Cause | Fix | Scope |",
|
|
1049
|
+
"|---|--------------|------------|-----|-------|",
|
|
1050
|
+
"",
|
|
1051
|
+
].join("\n");
|
|
1052
|
+
} else {
|
|
1053
|
+
content = header + [
|
|
1054
|
+
"## Rules",
|
|
1055
|
+
"",
|
|
1056
|
+
"| # | Scope | Rule | Why | Added |",
|
|
1057
|
+
"|---|-------|------|-----|-------|",
|
|
1058
|
+
"",
|
|
1059
|
+
"## Patterns",
|
|
1060
|
+
"",
|
|
1061
|
+
"| # | Pattern | Where | Notes |",
|
|
1062
|
+
"|---|---------|-------|-------|",
|
|
1063
|
+
"",
|
|
1064
|
+
"## Lessons Learned",
|
|
1065
|
+
"",
|
|
1066
|
+
"| # | What Happened | Root Cause | Fix | Scope |",
|
|
1067
|
+
"|---|--------------|------------|-----|-------|",
|
|
1068
|
+
`| L001 | ${entry} | — | — | ${scope} |`,
|
|
1069
|
+
"",
|
|
1070
|
+
].join("\n");
|
|
1071
|
+
}
|
|
1072
|
+
await saveFile(knowledgePath, content);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
954
1076
|
export async function loadActiveOverrides(basePath: string): Promise<Override[]> {
|
|
955
1077
|
const overridesPath = resolveGsdRootFile(basePath, "OVERRIDES");
|
|
956
1078
|
const content = await loadFile(overridesPath);
|
|
@@ -22,11 +22,12 @@ import {
|
|
|
22
22
|
} from "./paths.js";
|
|
23
23
|
import { randomInt } from "node:crypto";
|
|
24
24
|
import { join } from "node:path";
|
|
25
|
-
import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
|
|
25
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
|
|
26
26
|
import { nativeIsRepo, nativeInit, nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
|
|
27
27
|
import { ensureGitignore, ensurePreferences, untrackRuntimeFiles } from "./gitignore.js";
|
|
28
28
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
29
29
|
import { showConfirm } from "../shared/confirm-ui.js";
|
|
30
|
+
import { loadQueueOrder, sortByQueueOrder, saveQueueOrder } from "./queue-order.js";
|
|
30
31
|
|
|
31
32
|
// ─── Auto-start after discuss ─────────────────────────────────────────────────
|
|
32
33
|
|
|
@@ -203,13 +204,16 @@ function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string)
|
|
|
203
204
|
export function findMilestoneIds(basePath: string): string[] {
|
|
204
205
|
const dir = milestonesDir(basePath);
|
|
205
206
|
try {
|
|
206
|
-
|
|
207
|
+
const ids = readdirSync(dir, { withFileTypes: true })
|
|
207
208
|
.filter((d) => d.isDirectory())
|
|
208
209
|
.map((d) => {
|
|
209
210
|
const match = d.name.match(/^(M\d+(?:-[a-z0-9]{6})?)/);
|
|
210
211
|
return match ? match[1] : d.name;
|
|
211
|
-
})
|
|
212
|
-
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Apply custom queue order if available, else fall back to numeric sort
|
|
215
|
+
const customOrder = loadQueueOrder(basePath);
|
|
216
|
+
return sortByQueueOrder(ids, customOrder);
|
|
213
217
|
} catch {
|
|
214
218
|
return [];
|
|
215
219
|
}
|
|
@@ -305,6 +309,235 @@ export async function showQueue(
|
|
|
305
309
|
return;
|
|
306
310
|
}
|
|
307
311
|
|
|
312
|
+
// ── Count pending milestones ────────────────────────────────────────
|
|
313
|
+
const pendingMilestones = state.registry.filter(
|
|
314
|
+
m => m.status === "pending" || m.status === "active",
|
|
315
|
+
);
|
|
316
|
+
const completeCount = state.registry.filter(m => m.status === "complete").length;
|
|
317
|
+
|
|
318
|
+
// ── If multiple pending milestones, show queue management hub ──────
|
|
319
|
+
if (pendingMilestones.length > 1) {
|
|
320
|
+
const choice = await showNextAction(ctx, {
|
|
321
|
+
title: "GSD — Queue Management",
|
|
322
|
+
summary: [
|
|
323
|
+
`${completeCount} complete, ${pendingMilestones.length} pending.`,
|
|
324
|
+
],
|
|
325
|
+
actions: [
|
|
326
|
+
{
|
|
327
|
+
id: "reorder",
|
|
328
|
+
label: "Reorder queue",
|
|
329
|
+
description: `Change execution order of ${pendingMilestones.length} pending milestones.`,
|
|
330
|
+
recommended: true,
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: "add",
|
|
334
|
+
label: "Add new work",
|
|
335
|
+
description: "Queue new milestones via discussion.",
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
notYetMessage: "Run /gsd queue when ready.",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
if (choice === "reorder") {
|
|
342
|
+
await handleQueueReorder(ctx, basePath, state);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (choice === "not_yet") return;
|
|
346
|
+
// "add" falls through to existing queue-add logic below
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── Existing queue-add flow ─────────────────────────────────────────
|
|
350
|
+
await showQueueAdd(ctx, pi, basePath, state);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function handleQueueReorder(
|
|
354
|
+
ctx: ExtensionCommandContext,
|
|
355
|
+
basePath: string,
|
|
356
|
+
state: Awaited<ReturnType<typeof deriveState>>,
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
const { showQueueReorder: showReorderUI } = await import("./queue-reorder-ui.js");
|
|
359
|
+
const { invalidateStateCache } = await import("./state.js");
|
|
360
|
+
|
|
361
|
+
const completed = state.registry
|
|
362
|
+
.filter(m => m.status === "complete")
|
|
363
|
+
.map(m => ({ id: m.id, title: m.title, dependsOn: m.dependsOn }));
|
|
364
|
+
|
|
365
|
+
const pending = state.registry
|
|
366
|
+
.filter(m => m.status !== "complete")
|
|
367
|
+
.map(m => ({ id: m.id, title: m.title, dependsOn: m.dependsOn }));
|
|
368
|
+
|
|
369
|
+
const result = await showReorderUI(ctx, completed, pending);
|
|
370
|
+
if (!result) {
|
|
371
|
+
ctx.ui.notify("Queue reorder cancelled.", "info");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Save the new order
|
|
376
|
+
saveQueueOrder(basePath, result.order);
|
|
377
|
+
invalidateStateCache();
|
|
378
|
+
|
|
379
|
+
// Remove conflicting depends_on entries from CONTEXT.md files
|
|
380
|
+
if (result.depsToRemove.length > 0) {
|
|
381
|
+
removeDependsOnFromContextFiles(basePath, result.depsToRemove);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Sync PROJECT.md milestone sequence table
|
|
385
|
+
syncProjectMdSequence(basePath, state.registry, result.order);
|
|
386
|
+
|
|
387
|
+
// Commit the change
|
|
388
|
+
const filesToAdd = [".gsd/QUEUE-ORDER.json", ".gsd/PROJECT.md"];
|
|
389
|
+
for (const r of result.depsToRemove) {
|
|
390
|
+
filesToAdd.push(`.gsd/milestones/${r.milestone}/${r.milestone}-CONTEXT.md`);
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
nativeAddPaths(basePath, filesToAdd);
|
|
394
|
+
nativeCommit(basePath, "docs: reorder queue");
|
|
395
|
+
} catch {
|
|
396
|
+
// Commit may fail if nothing changed or git hooks block — non-fatal
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const depInfo = result.depsToRemove.length > 0
|
|
400
|
+
? ` (removed ${result.depsToRemove.length} depends_on)`
|
|
401
|
+
: "";
|
|
402
|
+
ctx.ui.notify(`Queue reordered: ${result.order.join(" → ")}${depInfo}`, "info");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Remove specific depends_on entries from milestone CONTEXT.md frontmatter.
|
|
407
|
+
*/
|
|
408
|
+
function removeDependsOnFromContextFiles(
|
|
409
|
+
basePath: string,
|
|
410
|
+
depsToRemove: Array<{ milestone: string; dep: string }>,
|
|
411
|
+
): void {
|
|
412
|
+
// Group removals by milestone
|
|
413
|
+
const byMilestone = new Map<string, string[]>();
|
|
414
|
+
for (const { milestone, dep } of depsToRemove) {
|
|
415
|
+
const existing = byMilestone.get(milestone) ?? [];
|
|
416
|
+
existing.push(dep);
|
|
417
|
+
byMilestone.set(milestone, existing);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
for (const [mid, depsToRemoveForMid] of byMilestone) {
|
|
421
|
+
const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
|
|
422
|
+
if (!contextFile || !existsSync(contextFile)) continue;
|
|
423
|
+
|
|
424
|
+
const content = readFileSync(contextFile, "utf-8");
|
|
425
|
+
|
|
426
|
+
// Parse frontmatter
|
|
427
|
+
const trimmed = content.trimStart();
|
|
428
|
+
if (!trimmed.startsWith("---")) continue;
|
|
429
|
+
const afterFirst = trimmed.indexOf("\n");
|
|
430
|
+
if (afterFirst === -1) continue;
|
|
431
|
+
const rest = trimmed.slice(afterFirst + 1);
|
|
432
|
+
const endIdx = rest.indexOf("\n---");
|
|
433
|
+
if (endIdx === -1) continue;
|
|
434
|
+
|
|
435
|
+
const fmText = rest.slice(0, endIdx);
|
|
436
|
+
const body = rest.slice(endIdx + 4);
|
|
437
|
+
|
|
438
|
+
// Parse depends_on line(s)
|
|
439
|
+
const fmLines = fmText.split("\n");
|
|
440
|
+
const removeSet = new Set(depsToRemoveForMid.map(d => d.toUpperCase()));
|
|
441
|
+
|
|
442
|
+
// Handle inline format: depends_on: [M009, M010]
|
|
443
|
+
const inlineMatch = fmLines.findIndex(l => /^depends_on:\s*\[/.test(l));
|
|
444
|
+
if (inlineMatch >= 0) {
|
|
445
|
+
const line = fmLines[inlineMatch];
|
|
446
|
+
const inner = line.match(/\[([^\]]*)\]/);
|
|
447
|
+
if (inner) {
|
|
448
|
+
const remaining = inner[1]
|
|
449
|
+
.split(",")
|
|
450
|
+
.map(s => s.trim())
|
|
451
|
+
.filter(s => s && !removeSet.has(s.toUpperCase()));
|
|
452
|
+
if (remaining.length === 0) {
|
|
453
|
+
fmLines.splice(inlineMatch, 1);
|
|
454
|
+
} else {
|
|
455
|
+
fmLines[inlineMatch] = `depends_on: [${remaining.join(", ")}]`;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
// Handle multi-line format
|
|
460
|
+
const keyIdx = fmLines.findIndex(l => /^depends_on:\s*$/.test(l));
|
|
461
|
+
if (keyIdx >= 0) {
|
|
462
|
+
let end = keyIdx + 1;
|
|
463
|
+
while (end < fmLines.length && /^\s+-\s/.test(fmLines[end])) {
|
|
464
|
+
const val = fmLines[end].replace(/^\s+-\s*/, "").trim().toUpperCase();
|
|
465
|
+
if (removeSet.has(val)) {
|
|
466
|
+
fmLines.splice(end, 1);
|
|
467
|
+
} else {
|
|
468
|
+
end++;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (end === keyIdx + 1 || (end <= fmLines.length && !/^\s+-\s/.test(fmLines[keyIdx + 1] ?? ""))) {
|
|
472
|
+
fmLines.splice(keyIdx, 1);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Rebuild file
|
|
478
|
+
const newFm = fmLines.filter(l => l !== undefined).join("\n");
|
|
479
|
+
const newContent = newFm.trim()
|
|
480
|
+
? `---\n${newFm}\n---${body}`
|
|
481
|
+
: body.replace(/^\n+/, "");
|
|
482
|
+
writeFileSync(contextFile, newContent, "utf-8");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function syncProjectMdSequence(
|
|
487
|
+
basePath: string,
|
|
488
|
+
registry: Array<{ id: string; title: string; status: string }>,
|
|
489
|
+
newOrder: string[],
|
|
490
|
+
): void {
|
|
491
|
+
const projectPath = resolveGsdRootFile(basePath, "PROJECT");
|
|
492
|
+
if (!projectPath || !existsSync(projectPath)) return;
|
|
493
|
+
|
|
494
|
+
const content = readFileSync(projectPath, "utf-8");
|
|
495
|
+
const lines = content.split("\n");
|
|
496
|
+
|
|
497
|
+
const headerIdx = lines.findIndex(l => /^##\s+Milestone Sequence/.test(l));
|
|
498
|
+
if (headerIdx < 0) return;
|
|
499
|
+
|
|
500
|
+
let tableStart = headerIdx + 1;
|
|
501
|
+
while (tableStart < lines.length && !lines[tableStart].startsWith("|")) tableStart++;
|
|
502
|
+
if (tableStart >= lines.length) return;
|
|
503
|
+
|
|
504
|
+
let tableEnd = tableStart + 1;
|
|
505
|
+
while (tableEnd < lines.length && lines[tableEnd].startsWith("|")) tableEnd++;
|
|
506
|
+
|
|
507
|
+
const registryMap = new Map(registry.map(m => [m.id, m]));
|
|
508
|
+
const completedSet = new Set(registry.filter(m => m.status === "complete").map(m => m.id));
|
|
509
|
+
|
|
510
|
+
const newRows: string[] = [];
|
|
511
|
+
for (const m of registry) {
|
|
512
|
+
if (m.status === "complete") {
|
|
513
|
+
newRows.push(`| ${m.id} | ${m.title} | ✅ Complete |`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
let isFirst = true;
|
|
517
|
+
for (const id of newOrder) {
|
|
518
|
+
if (completedSet.has(id)) continue;
|
|
519
|
+
const m = registryMap.get(id);
|
|
520
|
+
if (!m) continue;
|
|
521
|
+
const status = isFirst ? "📋 Next" : "📋 Queued";
|
|
522
|
+
newRows.push(`| ${m.id} | ${m.title} | ${status} |`);
|
|
523
|
+
isFirst = false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const headerLine = lines[tableStart];
|
|
527
|
+
const separatorLine = lines[tableStart + 1];
|
|
528
|
+
const newTable = [headerLine, separatorLine, ...newRows];
|
|
529
|
+
lines.splice(tableStart, tableEnd - tableStart, ...newTable);
|
|
530
|
+
writeFileSync(projectPath, lines.join("\n"), "utf-8");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function showQueueAdd(
|
|
534
|
+
ctx: ExtensionCommandContext,
|
|
535
|
+
pi: ExtensionAPI,
|
|
536
|
+
basePath: string,
|
|
537
|
+
state: Awaited<ReturnType<typeof deriveState>>,
|
|
538
|
+
): Promise<void> {
|
|
539
|
+
const milestoneIds = findMilestoneIds(basePath);
|
|
540
|
+
|
|
308
541
|
// ── Build existing milestones context for the prompt ────────────────
|
|
309
542
|
const existingContext = await buildExistingMilestonesContext(basePath, milestoneIds, state);
|
|
310
543
|
|
|
@@ -28,10 +28,11 @@ import { createBashTool, createWriteTool, createReadTool, createEditTool, isTool
|
|
|
28
28
|
import { registerGSDCommand, loadToolApiKeys } from "./commands.js";
|
|
29
29
|
import { registerExitCommand } from "./exit-command.js";
|
|
30
30
|
import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
|
|
31
|
+
import { getActiveAutoWorktreeContext } from "./auto-worktree.js";
|
|
31
32
|
import { saveFile, formatContinue, loadFile, parseContinue, parseSummary, loadActiveOverrides, formatOverridesSection } from "./files.js";
|
|
32
33
|
import { loadPrompt } from "./prompt-loader.js";
|
|
33
34
|
import { deriveState } from "./state.js";
|
|
34
|
-
import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData } from "./auto.js";
|
|
35
|
+
import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData, markToolStart, markToolEnd } from "./auto.js";
|
|
35
36
|
import { saveActivityLog } from "./activity-log.js";
|
|
36
37
|
import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId } from "./guided-flow.js";
|
|
37
38
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
@@ -47,10 +48,11 @@ import {
|
|
|
47
48
|
resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTaskFiles, resolveTasksDir,
|
|
48
49
|
relSliceFile, relSlicePath, relTaskFile,
|
|
49
50
|
buildSliceFileName, buildMilestoneFileName, gsdRoot, resolveMilestonePath,
|
|
51
|
+
resolveGsdRootFile,
|
|
50
52
|
} from "./paths.js";
|
|
51
53
|
import { Key } from "@gsd/pi-tui";
|
|
52
54
|
import { join } from "node:path";
|
|
53
|
-
import { existsSync } from "node:fs";
|
|
55
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
54
56
|
import { shortcutDesc } from "../shared/terminal.js";
|
|
55
57
|
import { Text } from "@gsd/pi-tui";
|
|
56
58
|
import { pauseAutoForProviderError } from "./provider-error-pause.js";
|
|
@@ -272,6 +274,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
276
|
|
|
277
|
+
// Load project knowledge if available
|
|
278
|
+
let knowledgeBlock = "";
|
|
279
|
+
const knowledgePath = resolveGsdRootFile(process.cwd(), "KNOWLEDGE");
|
|
280
|
+
if (existsSync(knowledgePath)) {
|
|
281
|
+
try {
|
|
282
|
+
const content = readFileSync(knowledgePath, "utf-8").trim();
|
|
283
|
+
if (content) {
|
|
284
|
+
knowledgeBlock = `\n\n[PROJECT KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${content}`;
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// File read error — skip knowledge injection
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
275
291
|
// Detect skills installed during this auto-mode session
|
|
276
292
|
let newSkillsBlock = "";
|
|
277
293
|
if (hasSkillSnapshot()) {
|
|
@@ -287,6 +303,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
287
303
|
let worktreeBlock = "";
|
|
288
304
|
const worktreeName = getActiveWorktreeName();
|
|
289
305
|
const worktreeMainCwd = getWorktreeOriginalCwd();
|
|
306
|
+
const autoWorktree = getActiveAutoWorktreeContext();
|
|
290
307
|
if (worktreeName && worktreeMainCwd) {
|
|
291
308
|
worktreeBlock = [
|
|
292
309
|
"",
|
|
@@ -304,10 +321,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
304
321
|
"All file operations, bash commands, and GSD state resolve against the worktree path above.",
|
|
305
322
|
"Use /worktree merge to merge changes back. Use /worktree return to switch back to the main tree.",
|
|
306
323
|
].join("\n");
|
|
324
|
+
} else if (autoWorktree) {
|
|
325
|
+
worktreeBlock = [
|
|
326
|
+
"",
|
|
327
|
+
"",
|
|
328
|
+
"[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
|
|
329
|
+
`IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
|
|
330
|
+
`The actual current working directory is: ${process.cwd()}`,
|
|
331
|
+
"",
|
|
332
|
+
"You are working inside a GSD auto-worktree.",
|
|
333
|
+
`- Milestone worktree: ${autoWorktree.worktreeName}`,
|
|
334
|
+
`- Worktree path (this is the real cwd): ${process.cwd()}`,
|
|
335
|
+
`- Main project: ${autoWorktree.originalBase}`,
|
|
336
|
+
`- Branch: ${autoWorktree.branch}`,
|
|
337
|
+
"",
|
|
338
|
+
"All file operations, bash commands, and GSD state resolve against the worktree path above.",
|
|
339
|
+
"Write every .gsd artifact in the worktree path above, never in the main project tree.",
|
|
340
|
+
].join("\n");
|
|
307
341
|
}
|
|
308
342
|
|
|
309
343
|
return {
|
|
310
|
-
systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${newSkillsBlock}${worktreeBlock}`,
|
|
344
|
+
systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${newSkillsBlock}${worktreeBlock}`,
|
|
311
345
|
...(injection
|
|
312
346
|
? {
|
|
313
347
|
message: {
|
|
@@ -542,6 +576,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
542
576
|
const existing = await loadFile(discussionPath) ?? `# ${milestoneId} Discussion Log\n\n`;
|
|
543
577
|
await saveFile(discussionPath, existing + newBlock);
|
|
544
578
|
});
|
|
579
|
+
|
|
580
|
+
// ── tool_execution_start/end: track in-flight tools for idle detection ──
|
|
581
|
+
pi.on("tool_execution_start", async (event) => {
|
|
582
|
+
if (!isAutoActive()) return;
|
|
583
|
+
markToolStart(event.toolCallId);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
pi.on("tool_execution_end", async (event) => {
|
|
587
|
+
markToolEnd(event.toolCallId);
|
|
588
|
+
});
|
|
545
589
|
}
|
|
546
590
|
|
|
547
591
|
async function buildGuidedExecuteContextInjection(prompt: string, basePath: string): Promise<string | null> {
|
|
@@ -39,6 +39,8 @@ export interface UnitMetrics {
|
|
|
39
39
|
toolCalls: number;
|
|
40
40
|
assistantMessages: number;
|
|
41
41
|
userMessages: number;
|
|
42
|
+
tier?: string; // complexity tier (light/standard/heavy) if dynamic routing active
|
|
43
|
+
modelDowngraded?: boolean; // true if dynamic routing used a cheaper model
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export interface MetricsLedger {
|
|
@@ -104,6 +106,7 @@ export function snapshotUnitMetrics(
|
|
|
104
106
|
unitId: string,
|
|
105
107
|
startedAt: number,
|
|
106
108
|
model: string,
|
|
109
|
+
extras?: { tier?: string; modelDowngraded?: boolean },
|
|
107
110
|
): UnitMetrics | null {
|
|
108
111
|
if (!ledger) return null;
|
|
109
112
|
|
|
@@ -156,6 +159,8 @@ export function snapshotUnitMetrics(
|
|
|
156
159
|
toolCalls,
|
|
157
160
|
assistantMessages,
|
|
158
161
|
userMessages,
|
|
162
|
+
...(extras?.tier ? { tier: extras.tier } : {}),
|
|
163
|
+
...(extras?.modelDowngraded !== undefined ? { modelDowngraded: extras.modelDowngraded } : {}),
|
|
159
164
|
};
|
|
160
165
|
|
|
161
166
|
ledger.units.push(unit);
|
|
@@ -294,6 +299,49 @@ export function getProjectTotals(units: UnitMetrics[]): ProjectTotals {
|
|
|
294
299
|
return totals;
|
|
295
300
|
}
|
|
296
301
|
|
|
302
|
+
// ─── Tier Aggregation ────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
export interface TierAggregate {
|
|
305
|
+
tier: string;
|
|
306
|
+
units: number;
|
|
307
|
+
tokens: TokenCounts;
|
|
308
|
+
cost: number;
|
|
309
|
+
downgraded: number; // units that were downgraded by dynamic routing
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function aggregateByTier(units: UnitMetrics[]): TierAggregate[] {
|
|
313
|
+
const map = new Map<string, TierAggregate>();
|
|
314
|
+
for (const u of units) {
|
|
315
|
+
const tier = u.tier ?? "unknown";
|
|
316
|
+
let agg = map.get(tier);
|
|
317
|
+
if (!agg) {
|
|
318
|
+
agg = { tier, units: 0, tokens: emptyTokens(), cost: 0, downgraded: 0 };
|
|
319
|
+
map.set(tier, agg);
|
|
320
|
+
}
|
|
321
|
+
agg.units++;
|
|
322
|
+
agg.tokens = addTokens(agg.tokens, u.tokens);
|
|
323
|
+
agg.cost += u.cost;
|
|
324
|
+
if (u.modelDowngraded) agg.downgraded++;
|
|
325
|
+
}
|
|
326
|
+
const order = ["light", "standard", "heavy", "unknown"];
|
|
327
|
+
return order.map(t => map.get(t)).filter((a): a is TierAggregate => !!a);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Format a summary of savings from dynamic routing.
|
|
332
|
+
* Returns empty string if no units were downgraded.
|
|
333
|
+
*/
|
|
334
|
+
export function formatTierSavings(units: UnitMetrics[]): string {
|
|
335
|
+
const downgraded = units.filter(u => u.modelDowngraded);
|
|
336
|
+
if (downgraded.length === 0) return "";
|
|
337
|
+
|
|
338
|
+
const downgradedCost = downgraded.reduce((sum, u) => sum + u.cost, 0);
|
|
339
|
+
const totalUnits = units.filter(u => u.tier).length;
|
|
340
|
+
const pct = totalUnits > 0 ? Math.round((downgraded.length / totalUnits) * 100) : 0;
|
|
341
|
+
|
|
342
|
+
return `Dynamic routing: ${downgraded.length}/${totalUnits} units downgraded (${pct}%), cost: ${formatCost(downgradedCost)}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
297
345
|
// ─── Formatting helpers ───────────────────────────────────────────────────────
|
|
298
346
|
|
|
299
347
|
export function formatCost(cost: number): string {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// GSD Extension — Model Cost Table
|
|
2
|
+
// Static cost reference for known models, used by the dynamic router
|
|
3
|
+
// for cross-provider cost comparison.
|
|
4
|
+
//
|
|
5
|
+
// Costs are approximate per-1K-token rates in USD (input tokens).
|
|
6
|
+
// Updated with GSD releases. Users can override via preferences.
|
|
7
|
+
|
|
8
|
+
export interface ModelCostEntry {
|
|
9
|
+
/** Model ID (bare, without provider prefix) */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Approximate cost per 1K input tokens in USD */
|
|
12
|
+
inputPer1k: number;
|
|
13
|
+
/** Approximate cost per 1K output tokens in USD */
|
|
14
|
+
outputPer1k: number;
|
|
15
|
+
/** Last updated date */
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Bundled cost table for known models.
|
|
21
|
+
* Updated periodically with GSD releases.
|
|
22
|
+
*/
|
|
23
|
+
export const BUNDLED_COST_TABLE: ModelCostEntry[] = [
|
|
24
|
+
// Anthropic
|
|
25
|
+
{ id: "claude-opus-4-6", inputPer1k: 0.015, outputPer1k: 0.075, updatedAt: "2025-03-15" },
|
|
26
|
+
{ id: "claude-sonnet-4-6", inputPer1k: 0.003, outputPer1k: 0.015, updatedAt: "2025-03-15" },
|
|
27
|
+
{ id: "claude-haiku-4-5", inputPer1k: 0.0008, outputPer1k: 0.004, updatedAt: "2025-03-15" },
|
|
28
|
+
{ id: "claude-sonnet-4-5-20250514", inputPer1k: 0.003, outputPer1k: 0.015, updatedAt: "2025-03-15" },
|
|
29
|
+
{ id: "claude-3-5-sonnet-latest", inputPer1k: 0.003, outputPer1k: 0.015, updatedAt: "2025-03-15" },
|
|
30
|
+
{ id: "claude-3-5-haiku-latest", inputPer1k: 0.0008, outputPer1k: 0.004, updatedAt: "2025-03-15" },
|
|
31
|
+
{ id: "claude-3-opus-latest", inputPer1k: 0.015, outputPer1k: 0.075, updatedAt: "2025-03-15" },
|
|
32
|
+
|
|
33
|
+
// OpenAI
|
|
34
|
+
{ id: "gpt-4o", inputPer1k: 0.0025, outputPer1k: 0.01, updatedAt: "2025-03-15" },
|
|
35
|
+
{ id: "gpt-4o-mini", inputPer1k: 0.00015, outputPer1k: 0.0006, updatedAt: "2025-03-15" },
|
|
36
|
+
{ id: "o1", inputPer1k: 0.015, outputPer1k: 0.06, updatedAt: "2025-03-15" },
|
|
37
|
+
{ id: "o3", inputPer1k: 0.015, outputPer1k: 0.06, updatedAt: "2025-03-15" },
|
|
38
|
+
{ id: "gpt-4-turbo", inputPer1k: 0.01, outputPer1k: 0.03, updatedAt: "2025-03-15" },
|
|
39
|
+
|
|
40
|
+
// Google
|
|
41
|
+
{ id: "gemini-2.0-flash", inputPer1k: 0.0001, outputPer1k: 0.0004, updatedAt: "2025-03-15" },
|
|
42
|
+
{ id: "gemini-flash-2.0", inputPer1k: 0.0001, outputPer1k: 0.0004, updatedAt: "2025-03-15" },
|
|
43
|
+
{ id: "gemini-2.5-pro", inputPer1k: 0.00125, outputPer1k: 0.005, updatedAt: "2025-03-15" },
|
|
44
|
+
|
|
45
|
+
// DeepSeek
|
|
46
|
+
{ id: "deepseek-chat", inputPer1k: 0.00014, outputPer1k: 0.00028, updatedAt: "2025-03-15" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Lookup cost for a model ID. Returns undefined if not found.
|
|
51
|
+
*/
|
|
52
|
+
export function lookupModelCost(modelId: string): ModelCostEntry | undefined {
|
|
53
|
+
const bareId = modelId.includes("/") ? modelId.split("/").pop()! : modelId;
|
|
54
|
+
return BUNDLED_COST_TABLE.find(e => e.id === bareId)
|
|
55
|
+
?? BUNDLED_COST_TABLE.find(e => bareId.includes(e.id) || e.id.includes(bareId));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compare two models by input cost. Returns negative if a is cheaper.
|
|
60
|
+
*/
|
|
61
|
+
export function compareModelCost(modelIdA: string, modelIdB: string): number {
|
|
62
|
+
const costA = lookupModelCost(modelIdA)?.inputPer1k ?? 999;
|
|
63
|
+
const costB = lookupModelCost(modelIdB)?.inputPer1k ?? 999;
|
|
64
|
+
return costA - costB;
|
|
65
|
+
}
|