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.
Files changed (217) hide show
  1. package/README.md +39 -0
  2. package/dist/onboarding.js +2 -2
  3. package/dist/remote-questions-config.d.ts +10 -0
  4. package/dist/remote-questions-config.js +36 -0
  5. package/dist/resources/extensions/gsd/activity-log.ts +37 -7
  6. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +65 -16
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  9. package/dist/resources/extensions/gsd/auto.ts +399 -29
  10. package/dist/resources/extensions/gsd/captures.ts +384 -0
  11. package/dist/resources/extensions/gsd/commands.ts +382 -23
  12. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  14. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  15. package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  16. package/dist/resources/extensions/gsd/files.ts +123 -1
  17. package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
  18. package/dist/resources/extensions/gsd/index.ts +47 -3
  19. package/dist/resources/extensions/gsd/metrics.ts +48 -0
  20. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  21. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  22. package/dist/resources/extensions/gsd/paths.ts +9 -0
  23. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  24. package/dist/resources/extensions/gsd/preferences.ts +132 -1
  25. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  26. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  27. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  28. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  29. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  30. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  31. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  32. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  33. package/dist/resources/extensions/gsd/state.ts +15 -3
  34. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  35. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  36. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  37. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  38. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  39. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  40. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  41. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  42. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  43. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  44. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  45. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  46. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  47. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  48. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  49. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  50. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  51. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  52. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  53. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  54. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  55. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  56. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  57. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  58. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  59. package/dist/resources/extensions/gsd/visualizer-data.ts +154 -0
  60. package/dist/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  61. package/dist/resources/extensions/gsd/visualizer-views.ts +293 -0
  62. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  63. package/dist/resources/extensions/gsd/worktree.ts +22 -0
  64. package/dist/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  65. package/dist/resources/extensions/remote-questions/format.ts +12 -6
  66. package/dist/resources/extensions/remote-questions/manager.ts +8 -0
  67. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  68. package/package.json +1 -1
  69. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  70. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  72. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  74. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  76. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  78. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  80. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  81. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  84. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  86. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  88. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  90. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  92. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  96. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  98. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  102. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  104. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  106. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  108. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  110. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  112. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  115. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  117. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/index.js +4 -1
  119. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/main.js +17 -2
  122. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  136. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  137. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  138. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  139. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  140. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  141. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  142. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  143. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  144. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  145. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  146. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  147. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  148. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  149. package/packages/pi-coding-agent/src/index.ts +5 -0
  150. package/packages/pi-coding-agent/src/main.ts +19 -2
  151. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  152. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  153. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  154. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  155. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  156. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  157. package/src/resources/extensions/gsd/auto-prompts.ts +65 -16
  158. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  159. package/src/resources/extensions/gsd/auto.ts +399 -29
  160. package/src/resources/extensions/gsd/captures.ts +384 -0
  161. package/src/resources/extensions/gsd/commands.ts +382 -23
  162. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  163. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  164. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  165. package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  166. package/src/resources/extensions/gsd/files.ts +123 -1
  167. package/src/resources/extensions/gsd/guided-flow.ts +237 -4
  168. package/src/resources/extensions/gsd/index.ts +47 -3
  169. package/src/resources/extensions/gsd/metrics.ts +48 -0
  170. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  171. package/src/resources/extensions/gsd/model-router.ts +256 -0
  172. package/src/resources/extensions/gsd/paths.ts +9 -0
  173. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  174. package/src/resources/extensions/gsd/preferences.ts +132 -1
  175. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  176. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  177. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  178. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  179. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  180. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  181. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  182. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  183. package/src/resources/extensions/gsd/state.ts +15 -3
  184. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  185. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  186. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  187. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  188. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  189. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  190. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  192. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  193. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  194. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  195. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  196. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  197. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  198. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  199. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  200. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +227 -1
  201. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  202. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  203. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  204. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  205. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +198 -0
  206. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +255 -0
  207. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  208. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  209. package/src/resources/extensions/gsd/visualizer-data.ts +154 -0
  210. package/src/resources/extensions/gsd/visualizer-overlay.ts +193 -0
  211. package/src/resources/extensions/gsd/visualizer-views.ts +293 -0
  212. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  213. package/src/resources/extensions/gsd/worktree.ts +22 -0
  214. package/src/resources/extensions/remote-questions/discord-adapter.ts +33 -0
  215. package/src/resources/extensions/remote-questions/format.ts +12 -6
  216. package/src/resources/extensions/remote-questions/manager.ts +8 -0
  217. 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).toUpperCase().trim()).filter(Boolean);
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
- return readdirSync(dir, { withFileTypes: true })
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
- .sort(milestoneIdSort);
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
+ }