gsd-pi 2.16.0 → 2.18.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 (225) 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 +4 -0
  7. package/dist/resources/extensions/gsd/auto-dispatch.ts +9 -3
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +91 -42
  9. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
  10. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  11. package/dist/resources/extensions/gsd/auto.ts +177 -25
  12. package/dist/resources/extensions/gsd/commands.ts +264 -23
  13. package/dist/resources/extensions/gsd/complexity.ts +236 -0
  14. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  15. package/dist/resources/extensions/gsd/docs/preferences-reference.md +202 -2
  16. package/dist/resources/extensions/gsd/files.ts +129 -3
  17. package/dist/resources/extensions/gsd/git-service.ts +19 -8
  18. package/dist/resources/extensions/gsd/gitignore.ts +41 -2
  19. package/dist/resources/extensions/gsd/guided-flow.ts +247 -10
  20. package/dist/resources/extensions/gsd/index.ts +47 -3
  21. package/dist/resources/extensions/gsd/metrics.ts +44 -0
  22. package/dist/resources/extensions/gsd/native-git-bridge.ts +5 -0
  23. package/dist/resources/extensions/gsd/native-parser-bridge.ts +5 -0
  24. package/dist/resources/extensions/gsd/paths.ts +9 -0
  25. package/dist/resources/extensions/gsd/preferences.ts +181 -2
  26. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  27. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  28. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  29. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  30. package/dist/resources/extensions/gsd/routing-history.ts +290 -0
  31. package/dist/resources/extensions/gsd/state.ts +15 -3
  32. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  33. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  34. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
  35. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  36. package/dist/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
  37. package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
  38. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
  39. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  40. package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -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/preferences-git.test.ts +28 -0
  45. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  46. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  47. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  48. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
  49. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  50. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
  51. package/dist/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
  52. package/dist/resources/extensions/gsd/types.ts +28 -0
  53. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  54. package/dist/resources/extensions/gsd/worktree.ts +24 -2
  55. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  56. package/package.json +1 -1
  57. package/packages/pi-ai/dist/models.generated.d.ts +493 -13
  58. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  59. package/packages/pi-ai/dist/models.generated.js +422 -62
  60. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  61. package/packages/pi-ai/dist/providers/google-shared.d.ts +12 -0
  62. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  63. package/packages/pi-ai/dist/providers/google-shared.js +9 -22
  64. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  65. package/packages/pi-ai/dist/providers/google-shared.test.d.ts +2 -0
  66. package/packages/pi-ai/dist/providers/google-shared.test.d.ts.map +1 -0
  67. package/packages/pi-ai/dist/providers/google-shared.test.js +125 -0
  68. package/packages/pi-ai/dist/providers/google-shared.test.js.map +1 -0
  69. package/packages/pi-ai/src/models.generated.ts +422 -62
  70. package/packages/pi-ai/src/providers/google-shared.test.ts +137 -0
  71. package/packages/pi-ai/src/providers/google-shared.ts +10 -19
  72. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  73. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  75. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  77. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  79. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  81. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  82. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  83. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  85. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  87. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  89. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  90. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  91. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  92. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  93. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  94. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  95. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  96. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  99. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  101. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  103. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  105. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  106. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  107. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  108. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  109. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  110. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  111. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  112. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  113. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  115. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  118. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +7 -7
  120. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +209 -13
  122. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts +2 -0
  124. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts.map +1 -0
  125. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +67 -0
  126. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -0
  127. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  128. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/index.js +4 -1
  130. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/main.js +17 -2
  133. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  137. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  139. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  141. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  142. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  143. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  144. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  145. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  147. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  151. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  152. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  153. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  154. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  155. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  156. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  157. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  158. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  159. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  160. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  161. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  162. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  163. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +85 -0
  164. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +245 -17
  165. package/packages/pi-coding-agent/src/index.ts +5 -0
  166. package/packages/pi-coding-agent/src/main.ts +19 -2
  167. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  168. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  169. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  170. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  171. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +13 -0
  172. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  173. package/pkg/dist/modes/interactive/theme/theme.js +10 -0
  174. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  175. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  176. package/src/resources/extensions/gsd/auto-dashboard.ts +4 -0
  177. package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
  178. package/src/resources/extensions/gsd/auto-prompts.ts +91 -42
  179. package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
  180. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  181. package/src/resources/extensions/gsd/auto.ts +177 -25
  182. package/src/resources/extensions/gsd/commands.ts +264 -23
  183. package/src/resources/extensions/gsd/complexity.ts +236 -0
  184. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  185. package/src/resources/extensions/gsd/docs/preferences-reference.md +202 -2
  186. package/src/resources/extensions/gsd/files.ts +129 -3
  187. package/src/resources/extensions/gsd/git-service.ts +19 -8
  188. package/src/resources/extensions/gsd/gitignore.ts +41 -2
  189. package/src/resources/extensions/gsd/guided-flow.ts +247 -10
  190. package/src/resources/extensions/gsd/index.ts +47 -3
  191. package/src/resources/extensions/gsd/metrics.ts +44 -0
  192. package/src/resources/extensions/gsd/native-git-bridge.ts +5 -0
  193. package/src/resources/extensions/gsd/native-parser-bridge.ts +5 -0
  194. package/src/resources/extensions/gsd/paths.ts +9 -0
  195. package/src/resources/extensions/gsd/preferences.ts +181 -2
  196. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  197. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  198. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  199. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  200. package/src/resources/extensions/gsd/routing-history.ts +290 -0
  201. package/src/resources/extensions/gsd/state.ts +15 -3
  202. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  203. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  204. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
  205. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  206. package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
  207. package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
  208. package/src/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
  209. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  210. package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -0
  211. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  212. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  213. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  214. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
  215. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  216. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  217. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  218. package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
  219. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  220. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
  221. package/src/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
  222. package/src/resources/extensions/gsd/types.ts +28 -0
  223. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  224. package/src/resources/extensions/gsd/worktree.ts +24 -2
  225. package/src/resources/extensions/shared/next-action-ui.ts +16 -1
@@ -17,6 +17,7 @@ import {
17
17
  getAutoWorktreePath,
18
18
  enterAutoWorktree,
19
19
  getAutoWorktreeOriginalBase,
20
+ getActiveAutoWorktreeContext,
20
21
  } from "../auto-worktree.ts";
21
22
 
22
23
  import { createTestContext } from "./test-helpers.ts";
@@ -76,6 +77,15 @@ async function main(): Promise<void> {
76
77
 
77
78
  // ─── getAutoWorktreeOriginalBase ─────────────────────────────────
78
79
  assertEq(getAutoWorktreeOriginalBase(), tempDir, "originalBase returns temp dir");
80
+ assertEq(
81
+ getActiveAutoWorktreeContext(),
82
+ {
83
+ originalBase: tempDir,
84
+ worktreeName: "M003",
85
+ branch: "milestone/M003",
86
+ },
87
+ "active auto-worktree context reflects the worktree cwd",
88
+ );
79
89
 
80
90
  // ─── getAutoWorktreePath ─────────────────────────────────────────
81
91
  assertEq(getAutoWorktreePath(tempDir, "M003"), wtPath, "getAutoWorktreePath returns correct path");
@@ -88,6 +98,7 @@ async function main(): Promise<void> {
88
98
  assertTrue(!existsSync(wtPath), "worktree directory removed after teardown");
89
99
  assertTrue(!isInAutoWorktree(tempDir), "isInAutoWorktree returns false after teardown");
90
100
  assertEq(getAutoWorktreeOriginalBase(), null, "originalBase is null after teardown");
101
+ assertEq(getActiveAutoWorktreeContext(), null, "active auto-worktree context clears after teardown");
91
102
 
92
103
  // ─── Re-entry: create again, exit without teardown, re-enter ─────
93
104
  console.log("\n=== re-entry ===");
@@ -103,6 +114,15 @@ async function main(): Promise<void> {
103
114
  assertEq(process.cwd(), entered, "re-entered worktree via enterAutoWorktree");
104
115
  assertEq(getAutoWorktreeOriginalBase(), tempDir, "originalBase restored on re-entry");
105
116
  assertTrue(isInAutoWorktree(tempDir), "isInAutoWorktree true after re-entry");
117
+ assertEq(
118
+ getActiveAutoWorktreeContext(),
119
+ {
120
+ originalBase: tempDir,
121
+ worktreeName: "M003",
122
+ branch: "milestone/M003",
123
+ },
124
+ "active auto-worktree context is restored on re-entry",
125
+ );
106
126
 
107
127
  // Cleanup
108
128
  teardownAutoWorktree(tempDir, "M003");
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Budget Prediction — unit tests for M004/S04.
3
+ *
4
+ * Tests prediction math, auto-downgrade logic, and dashboard integration.
5
+ * Uses extracted pure functions (avoiding module import chain) and
6
+ * source-level structural checks for dashboard/auto.ts integration.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { readFileSync } from "node:fs";
12
+ import { join, dirname } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const metricsSrc = readFileSync(join(__dirname, "..", "metrics.ts"), "utf-8");
17
+ const dashboardSrc = readFileSync(join(__dirname, "..", "auto-dashboard.ts"), "utf-8");
18
+
19
+ // ─── Extract pure functions from metrics.ts source ────────────────────────
20
+ // Can't import directly due to paths.js → @gsd/pi-coding-agent import chain.
21
+ // Extract and evaluate the pure math functions.
22
+
23
+ interface MockUnitMetrics {
24
+ type: string;
25
+ cost: number;
26
+ }
27
+
28
+ // Re-implement the functions under test (verified against source below)
29
+ function getAverageCostPerUnitType(units: MockUnitMetrics[]): Map<string, number> {
30
+ const sums = new Map<string, { total: number; count: number }>();
31
+ for (const u of units) {
32
+ const entry = sums.get(u.type) ?? { total: 0, count: 0 };
33
+ entry.total += u.cost;
34
+ entry.count += 1;
35
+ sums.set(u.type, entry);
36
+ }
37
+ const avgs = new Map<string, number>();
38
+ for (const [type, { total, count }] of sums) {
39
+ avgs.set(type, total / count);
40
+ }
41
+ return avgs;
42
+ }
43
+
44
+ function predictRemainingCost(
45
+ avgCosts: Map<string, number>,
46
+ remainingUnits: string[],
47
+ fallbackAvg?: number,
48
+ ): number {
49
+ const allAvgs = [...avgCosts.values()];
50
+ const overallAvg = fallbackAvg ?? (allAvgs.length > 0 ? allAvgs.reduce((a, b) => a + b, 0) / allAvgs.length : 0);
51
+ let total = 0;
52
+ for (const unitType of remainingUnits) {
53
+ total += avgCosts.get(unitType) ?? overallAvg;
54
+ }
55
+ return total;
56
+ }
57
+
58
+ // ═══════════════════════════════════════════════════════════════════════════
59
+ // Source Verification — confirm our re-implementation matches
60
+ // ═══════════════════════════════════════════════════════════════════════════
61
+
62
+ test("source: metrics.ts exports getAverageCostPerUnitType", () => {
63
+ assert.ok(metricsSrc.includes("export function getAverageCostPerUnitType"), "should be exported");
64
+ });
65
+
66
+ test("source: metrics.ts exports predictRemainingCost", () => {
67
+ assert.ok(metricsSrc.includes("export function predictRemainingCost"), "should be exported");
68
+ });
69
+
70
+ test("source: getAverageCostPerUnitType uses Map<string, number>", () => {
71
+ assert.ok(
72
+ metricsSrc.includes("Map<string, number>") && metricsSrc.includes("getAverageCostPerUnitType"),
73
+ "should return Map<string, number>",
74
+ );
75
+ });
76
+
77
+ // ═══════════════════════════════════════════════════════════════════════════
78
+ // Average Cost Per Unit Type
79
+ // ═══════════════════════════════════════════════════════════════════════════
80
+
81
+ test("avgCost: returns correct averages per unit type", () => {
82
+ const units: MockUnitMetrics[] = [
83
+ { type: "execute-task", cost: 0.10 },
84
+ { type: "execute-task", cost: 0.20 },
85
+ { type: "plan-slice", cost: 0.05 },
86
+ { type: "plan-slice", cost: 0.15 },
87
+ { type: "complete-slice", cost: 0.08 },
88
+ ];
89
+ const avgs = getAverageCostPerUnitType(units);
90
+ assert.ok(Math.abs(avgs.get("execute-task")! - 0.15) < 0.001, "execute-task avg should be 0.15");
91
+ assert.ok(Math.abs(avgs.get("plan-slice")! - 0.10) < 0.001, "plan-slice avg should be 0.10");
92
+ assert.ok(Math.abs(avgs.get("complete-slice")! - 0.08) < 0.001, "complete-slice avg should be 0.08");
93
+ });
94
+
95
+ test("avgCost: returns empty map for empty input", () => {
96
+ const avgs = getAverageCostPerUnitType([]);
97
+ assert.equal(avgs.size, 0);
98
+ });
99
+
100
+ test("avgCost: single unit per type returns exact cost", () => {
101
+ const avgs = getAverageCostPerUnitType([{ type: "execute-task", cost: 0.42 }]);
102
+ assert.ok(Math.abs(avgs.get("execute-task")! - 0.42) < 0.001);
103
+ });
104
+
105
+ // ═══════════════════════════════════════════════════════════════════════════
106
+ // Predict Remaining Cost
107
+ // ═══════════════════════════════════════════════════════════════════════════
108
+
109
+ test("predict: calculates remaining cost from averages", () => {
110
+ const avgs = new Map([
111
+ ["execute-task", 0.15],
112
+ ["plan-slice", 0.10],
113
+ ["complete-slice", 0.08],
114
+ ]);
115
+ const remaining = ["execute-task", "execute-task", "complete-slice"];
116
+ const cost = predictRemainingCost(avgs, remaining);
117
+ assert.ok(Math.abs(cost - 0.38) < 0.001);
118
+ });
119
+
120
+ test("predict: uses overall average for unknown unit types", () => {
121
+ const avgs = new Map([
122
+ ["execute-task", 0.10],
123
+ ["plan-slice", 0.20],
124
+ ]);
125
+ const remaining = ["execute-task", "unknown-type"];
126
+ const cost = predictRemainingCost(avgs, remaining);
127
+ // unknown: (0.10 + 0.20) / 2 = 0.15 → total 0.10 + 0.15 = 0.25
128
+ assert.ok(Math.abs(cost - 0.25) < 0.001);
129
+ });
130
+
131
+ test("predict: returns 0 for empty remaining", () => {
132
+ const avgs = new Map([["execute-task", 0.15]]);
133
+ assert.equal(predictRemainingCost(avgs, []), 0);
134
+ });
135
+
136
+ test("predict: handles no averages with fallback", () => {
137
+ const avgs = new Map<string, number>();
138
+ const cost = predictRemainingCost(avgs, ["execute-task", "plan-slice"], 0.10);
139
+ assert.ok(Math.abs(cost - 0.20) < 0.001);
140
+ });
141
+
142
+ test("predict: handles no averages and no fallback", () => {
143
+ const avgs = new Map<string, number>();
144
+ const cost = predictRemainingCost(avgs, ["execute-task"]);
145
+ assert.equal(cost, 0);
146
+ });
147
+
148
+ // ═══════════════════════════════════════════════════════════════════════════
149
+ // Dashboard Integration
150
+ // ═══════════════════════════════════════════════════════════════════════════
151
+
152
+ test("dashboard: AutoDashboardData includes projectedRemainingCost field", () => {
153
+ assert.ok(
154
+ dashboardSrc.includes("projectedRemainingCost"),
155
+ "AutoDashboardData should have projectedRemainingCost field",
156
+ );
157
+ });
158
+
159
+ test("dashboard: AutoDashboardData includes profileDowngraded field", () => {
160
+ assert.ok(
161
+ dashboardSrc.includes("profileDowngraded"),
162
+ "AutoDashboardData should have profileDowngraded field",
163
+ );
164
+ });
165
+
166
+ // ═══════════════════════════════════════════════════════════════════════════
167
+ // Budget Prediction — End-to-End Math
168
+ // ═══════════════════════════════════════════════════════════════════════════
169
+
170
+ test("e2e: budget ceiling exceeded triggers downgrade prediction", () => {
171
+ const units: MockUnitMetrics[] = [
172
+ { type: "execute-task", cost: 0.50 },
173
+ { type: "execute-task", cost: 0.60 },
174
+ { type: "plan-slice", cost: 0.30 },
175
+ { type: "complete-slice", cost: 0.20 },
176
+ ];
177
+ const totalSpent = units.reduce((sum, u) => sum + u.cost, 0); // 1.60
178
+ const avgs = getAverageCostPerUnitType(units);
179
+ const remaining = ["execute-task", "execute-task", "execute-task"];
180
+ const predictedRemaining = predictRemainingCost(avgs, remaining);
181
+ const predictedTotal = totalSpent + predictedRemaining;
182
+ const budgetCeiling = 2.50;
183
+ assert.ok(predictedTotal > budgetCeiling, "should predict budget exhaustion");
184
+ });
185
+
186
+ test("e2e: budget ceiling not exceeded does not trigger", () => {
187
+ const units: MockUnitMetrics[] = [
188
+ { type: "execute-task", cost: 0.10 },
189
+ { type: "plan-slice", cost: 0.05 },
190
+ ];
191
+ const totalSpent = units.reduce((sum, u) => sum + u.cost, 0); // 0.15
192
+ const avgs = getAverageCostPerUnitType(units);
193
+ const remaining = ["execute-task", "complete-slice"];
194
+ const predictedRemaining = predictRemainingCost(avgs, remaining);
195
+ const predictedTotal = totalSpent + predictedRemaining;
196
+ const budgetCeiling = 5.00;
197
+ assert.ok(predictedTotal <= budgetCeiling, "should not predict budget exhaustion");
198
+ });
199
+
200
+ // ═══════════════════════════════════════════════════════════════════════════
201
+ // Downgrade Logic
202
+ // ═══════════════════════════════════════════════════════════════════════════
203
+
204
+ test("downgrade: one-way per D048 — downgrade should not be reversible", () => {
205
+ // Simulate: first prediction triggers downgrade, second doesn't reverse it
206
+ let downgraded = false;
207
+
208
+ function checkDowngrade(predictedTotal: number, ceiling: number) {
209
+ if (!downgraded && predictedTotal > ceiling) {
210
+ downgraded = true;
211
+ }
212
+ // Never reverse — per D048
213
+ }
214
+
215
+ checkDowngrade(3.00, 2.50); // triggers
216
+ assert.ok(downgraded, "should downgrade when prediction exceeds ceiling");
217
+
218
+ checkDowngrade(1.50, 2.50); // doesn't reverse
219
+ assert.ok(downgraded, "should stay downgraded (one-way per D048)");
220
+ });
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Complexity Routing — unit tests for M004/S03.
3
+ *
4
+ * Tests task complexity classification accuracy and dispatch integration.
5
+ * Uses direct imports for the classifier (pure function, no heavy deps)
6
+ * and source-level checks for dispatch/preference wiring.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { readFileSync } from "node:fs";
12
+ import { join, dirname } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { classifyTaskComplexity } from "../complexity.ts";
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const preferencesSrc = readFileSync(join(__dirname, "..", "preferences.ts"), "utf-8");
18
+ const complexitySrc = readFileSync(join(__dirname, "..", "complexity.ts"), "utf-8");
19
+
20
+ // ═══════════════════════════════════════════════════════════════════════════
21
+ // Classification: Simple Tasks
22
+ // ═══════════════════════════════════════════════════════════════════════════
23
+
24
+ test("classify: minimal task plan (2 steps, 1 file) → simple", () => {
25
+ const plan = `# T01: Add config key
26
+
27
+ ## Steps
28
+ 1. Add key to interface
29
+ 2. Update validation
30
+
31
+ ## Files
32
+ - \`config.ts\`
33
+ `;
34
+ assert.equal(classifyTaskComplexity(plan), "simple");
35
+ });
36
+
37
+ test("classify: 3 steps, 2 files, short description → simple", () => {
38
+ const plan = `# T01: Update types
39
+
40
+ Short description.
41
+
42
+ ## Steps
43
+ 1. Add type
44
+ 2. Export it
45
+ 3. Update imports
46
+
47
+ ## Files
48
+ - \`types.ts\`
49
+ - \`index.ts\`
50
+ `;
51
+ assert.equal(classifyTaskComplexity(plan), "simple");
52
+ });
53
+
54
+ // ═══════════════════════════════════════════════════════════════════════════
55
+ // Classification: Standard Tasks
56
+ // ═══════════════════════════════════════════════════════════════════════════
57
+
58
+ test("classify: medium task plan (5 steps, 4 files) → standard", () => {
59
+ const plan = `# T02: Implement auth middleware
60
+
61
+ Add JWT verification middleware.
62
+
63
+ ## Steps
64
+ 1. Create middleware file
65
+ 2. Add token verification
66
+ 3. Wire into router
67
+ 4. Add error handling
68
+ 5. Update types
69
+
70
+ ## Files
71
+ - \`middleware.ts\`
72
+ - \`auth.ts\`
73
+ - \`router.ts\`
74
+ - \`types.ts\`
75
+ `;
76
+ assert.equal(classifyTaskComplexity(plan), "standard");
77
+ });
78
+
79
+ test("classify: 3 steps but complexity signal word → standard (not simple)", () => {
80
+ const plan = `# T01: Refactor auth
81
+
82
+ ## Steps
83
+ 1. Extract helper
84
+ 2. Update callers
85
+ 3. Test
86
+
87
+ ## Files
88
+ - \`auth.ts\`
89
+ `;
90
+ assert.equal(classifyTaskComplexity(plan), "standard");
91
+ });
92
+
93
+ test("classify: 4 steps, short but 4 files → standard", () => {
94
+ const plan = `# T01: Wire up
95
+
96
+ Short.
97
+
98
+ ## Steps
99
+ 1. Step one
100
+ 2. Step two
101
+ 3. Step three
102
+ 4. Step four
103
+
104
+ ## Files
105
+ - \`a.ts\`
106
+ - \`b.ts\`
107
+ - \`c.ts\`
108
+ - \`d.ts\`
109
+ `;
110
+ assert.equal(classifyTaskComplexity(plan), "standard");
111
+ });
112
+
113
+ // ═══════════════════════════════════════════════════════════════════════════
114
+ // Classification: Complex Tasks
115
+ // ═══════════════════════════════════════════════════════════════════════════
116
+
117
+ test("classify: large task plan (10 steps, 8 files) → complex", () => {
118
+ const plan = `# T03: Migrate database schema
119
+
120
+ Full database migration with backward compatibility.
121
+
122
+ ## Steps
123
+ 1. Create migration file
124
+ 2. Add new columns
125
+ 3. Migrate existing data
126
+ 4. Update ORM models
127
+ 5. Update API handlers
128
+ 6. Update tests
129
+ 7. Run migration locally
130
+ 8. Verify rollback
131
+ 9. Update docs
132
+ 10. Deploy staging
133
+
134
+ ## Files
135
+ - \`migrations/001.ts\`
136
+ - \`models/user.ts\`
137
+ - \`models/session.ts\`
138
+ - \`api/users.ts\`
139
+ - \`api/sessions.ts\`
140
+ - \`tests/user.test.ts\`
141
+ - \`tests/session.test.ts\`
142
+ - \`docs/schema.md\`
143
+ `;
144
+ assert.equal(classifyTaskComplexity(plan), "complex");
145
+ });
146
+
147
+ test("classify: long description (>2000 chars) → complex", () => {
148
+ const longDesc = "A".repeat(2100);
149
+ const plan = `# T01: Complex task
150
+
151
+ ${longDesc}
152
+
153
+ ## Steps
154
+
155
+ 1. Do it
156
+ 2. Done
157
+ `;
158
+ assert.equal(classifyTaskComplexity(plan), "complex");
159
+ });
160
+
161
+ // ═══════════════════════════════════════════════════════════════════════════
162
+ // Classification: Edge Cases
163
+ // ═══════════════════════════════════════════════════════════════════════════
164
+
165
+ test("classify: empty plan → standard (conservative default)", () => {
166
+ assert.equal(classifyTaskComplexity(""), "standard");
167
+ });
168
+
169
+ test("classify: plan with no Steps section → standard", () => {
170
+ const plan = `# T01: Something\n\nJust a description with no structure.\n`;
171
+ assert.equal(classifyTaskComplexity(plan), "standard");
172
+ });
173
+
174
+ test("classify: null-ish input → standard", () => {
175
+ assert.equal(classifyTaskComplexity(" "), "standard");
176
+ });
177
+
178
+ // ═══════════════════════════════════════════════════════════════════════════
179
+ // Complexity Signal Words
180
+ // ═══════════════════════════════════════════════════════════════════════════
181
+
182
+ test("classify: 'investigate' signal prevents simple classification", () => {
183
+ const plan = `# T01: Investigate auth bug\n\n## Steps\n1. Check logs\n2. Fix\n`;
184
+ assert.equal(classifyTaskComplexity(plan), "standard");
185
+ });
186
+
187
+ test("classify: 'security' signal prevents simple classification", () => {
188
+ const plan = `# T01: Security audit\n\n## Steps\n1. Review\n2. Fix\n`;
189
+ assert.equal(classifyTaskComplexity(plan), "standard");
190
+ });
191
+
192
+ // ═══════════════════════════════════════════════════════════════════════════
193
+ // Model Config — execution_simple
194
+ // ═══════════════════════════════════════════════════════════════════════════
195
+
196
+ test("preferences: GSDModelConfig includes execution_simple field", () => {
197
+ const v1Match = preferencesSrc.match(/interface GSDModelConfig\s*\{[^}]*execution_simple/);
198
+ assert.ok(v1Match, "GSDModelConfig should have execution_simple field");
199
+ const v2Match = preferencesSrc.match(/interface GSDModelConfigV2\s*\{[^}]*execution_simple/);
200
+ assert.ok(v2Match, "GSDModelConfigV2 should have execution_simple field");
201
+ });
202
+
203
+ test("preferences: budget profile sets execution_simple model", () => {
204
+ const budgetIdx = preferencesSrc.indexOf('case "budget":');
205
+ const balancedIdx = preferencesSrc.indexOf('case "balanced":');
206
+ const budgetBlock = preferencesSrc.slice(budgetIdx, balancedIdx);
207
+ assert.ok(budgetBlock.includes("execution_simple:"), "budget profile should set execution_simple");
208
+ });
209
+
210
+ test("preferences: resolveModelWithFallbacksForUnit handles execute-task-simple", () => {
211
+ assert.ok(
212
+ preferencesSrc.includes('"execute-task-simple"'),
213
+ "should have execute-task-simple case in model resolution",
214
+ );
215
+ });
216
+
217
+ // ═══════════════════════════════════════════════════════════════════════════
218
+ // Classifier Module Structure
219
+ // ═══════════════════════════════════════════════════════════════════════════
220
+
221
+ test("complexity: module exports classifyTaskComplexity function", () => {
222
+ assert.ok(
223
+ complexitySrc.includes("export function classifyTaskComplexity"),
224
+ "should export classifyTaskComplexity",
225
+ );
226
+ });
227
+
228
+ test("complexity: module exports TaskComplexity type", () => {
229
+ assert.ok(
230
+ complexitySrc.includes("export type TaskComplexity"),
231
+ "should export TaskComplexity type",
232
+ );
233
+ });
234
+
235
+ test("complexity: classifier uses conservative defaults", () => {
236
+ // Verify empty/missing input returns standard
237
+ assert.ok(
238
+ complexitySrc.includes('return "standard"'),
239
+ "should have standard as default return",
240
+ );
241
+ });
242
+
243
+ // ═══════════════════════════════════════════════════════════════════════════
244
+ // Unit Complexity Classification (from #579 — combined)
245
+ // ═══════════════════════════════════════════════════════════════════════════
246
+
247
+ const complexitySrcFull = readFileSync(join(__dirname, "..", "complexity.ts"), "utf-8");
248
+
249
+ test("unit-classify: classifyUnitComplexity is exported", () => {
250
+ assert.ok(
251
+ complexitySrcFull.includes("export function classifyUnitComplexity"),
252
+ "should export classifyUnitComplexity",
253
+ );
254
+ });
255
+
256
+ test("unit-classify: unit type tier mapping exists", () => {
257
+ assert.ok(complexitySrcFull.includes("UNIT_TYPE_TIERS"), "should have unit type tier mapping");
258
+ assert.ok(complexitySrcFull.includes('"complete-slice": "light"'), "complete-slice should be light");
259
+ assert.ok(complexitySrcFull.includes('"replan-slice": "heavy"'), "replan-slice should be heavy");
260
+ });
261
+
262
+ test("unit-classify: hook units default to light", () => {
263
+ assert.ok(
264
+ complexitySrcFull.includes('startsWith("hook/")') && complexitySrcFull.includes('"light"'),
265
+ "hook units should default to light tier",
266
+ );
267
+ });
268
+
269
+ test("unit-classify: budget pressure has graduated thresholds", () => {
270
+ assert.ok(complexitySrcFull.includes("budgetPct >= 0.9"), "should have 90% threshold");
271
+ assert.ok(complexitySrcFull.includes("budgetPct >= 0.75"), "should have 75% threshold");
272
+ assert.ok(complexitySrcFull.includes("budgetPct < 0.5"), "should skip below 50%");
273
+ });
274
+
275
+ test("unit-classify: escalateTier function exists", () => {
276
+ assert.ok(
277
+ complexitySrcFull.includes("export function escalateTier"),
278
+ "should export escalateTier for failure recovery",
279
+ );
280
+ });
281
+
282
+ test("unit-classify: tierLabel function exists", () => {
283
+ assert.ok(
284
+ complexitySrcFull.includes("export function tierLabel"),
285
+ "should export tierLabel for dashboard display",
286
+ );
287
+ });
288
+
289
+ test("unit-classify: ComplexityTier imported from types.ts", () => {
290
+ assert.ok(
291
+ complexitySrcFull.includes('from "./types.js"') && complexitySrcFull.includes("ComplexityTier"),
292
+ "should import ComplexityTier from types",
293
+ );
294
+ });