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
@@ -12,7 +12,8 @@ import { fileURLToPath } from "node:url";
12
12
  import { deriveState } from "./state.js";
13
13
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
14
14
  import { showQueue, showDiscuss } from "./guided-flow.js";
15
- import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode } from "./auto.js";
15
+ import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
16
+ import { resolveProjectRoot } from "./worktree.js";
16
17
  import {
17
18
  getGlobalGSDPreferencesPath,
18
19
  getLegacyGlobalGSDPreferencesPath,
@@ -22,7 +23,7 @@ import {
22
23
  loadEffectiveGSDPreferences,
23
24
  resolveAllSkillReferences,
24
25
  } from "./preferences.js";
25
- import { loadFile, saveFile, appendOverride } from "./files.js";
26
+ import { loadFile, saveFile, appendOverride, appendKnowledge } from "./files.js";
26
27
  import {
27
28
  formatDoctorIssuesForPrompt,
28
29
  formatDoctorReport,
@@ -56,14 +57,19 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
56
57
  );
57
58
  }
58
59
 
60
+ /** Resolve the effective project root, accounting for worktree paths. */
61
+ function projectRoot(): string {
62
+ return resolveProjectRoot(process.cwd());
63
+ }
64
+
59
65
  export function registerGSDCommand(pi: ExtensionAPI): void {
60
66
  pi.registerCommand("gsd", {
61
- description: "GSD — Get Shit Done: /gsd next|auto|stop|pause|status|queue|history|undo|skip|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer",
67
+ description: "GSD — Get Shit Done: /gsd next|auto|stop|pause|status|queue|history|undo|skip|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer|knowledge",
62
68
  getArgumentCompletions: (prefix: string) => {
63
69
  const subcommands = [
64
70
  "next", "auto", "stop", "pause", "status", "queue", "discuss",
65
71
  "history", "undo", "skip", "export", "cleanup", "prefs",
66
- "config", "hooks", "doctor", "migrate", "remote", "steer",
72
+ "config", "hooks", "doctor", "migrate", "remote", "steer", "knowledge",
67
73
  ];
68
74
  const parts = prefix.trim().split(/\s+/);
69
75
 
@@ -126,6 +132,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
126
132
  .map((cmd) => ({ value: `cleanup ${cmd}`, label: cmd }));
127
133
  }
128
134
 
135
+ if (parts[0] === "knowledge" && parts.length <= 2) {
136
+ const subPrefix = parts[1] ?? "";
137
+ return ["rule", "pattern", "lesson"]
138
+ .filter((cmd) => cmd.startsWith(subPrefix))
139
+ .map((cmd) => ({ value: `knowledge ${cmd}`, label: cmd }));
140
+ }
141
+
129
142
  if (parts[0] === "doctor") {
130
143
  const modePrefix = parts[1] ?? "";
131
144
  const modes = ["fix", "heal", "audit"];
@@ -162,23 +175,31 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
162
175
 
163
176
  if (trimmed === "next" || trimmed.startsWith("next ")) {
164
177
  if (trimmed.includes("--dry-run")) {
165
- await handleDryRun(ctx, process.cwd());
178
+ await handleDryRun(ctx, projectRoot());
166
179
  return;
167
180
  }
168
181
  const verboseMode = trimmed.includes("--verbose");
169
- await startAuto(ctx, pi, process.cwd(), verboseMode, { step: true });
182
+ await startAuto(ctx, pi, projectRoot(), verboseMode, { step: true });
170
183
  return;
171
184
  }
172
185
 
173
186
  if (trimmed === "auto" || trimmed.startsWith("auto ")) {
174
187
  const verboseMode = trimmed.includes("--verbose");
175
- await startAuto(ctx, pi, process.cwd(), verboseMode);
188
+ await startAuto(ctx, pi, projectRoot(), verboseMode);
176
189
  return;
177
190
  }
178
191
 
179
192
  if (trimmed === "stop") {
180
193
  if (!isAutoActive() && !isAutoPaused()) {
181
- ctx.ui.notify("Auto-mode is not running.", "info");
194
+ // Not running in this process — check for a remote auto-mode session
195
+ const result = stopAutoRemote(projectRoot());
196
+ if (result.found) {
197
+ ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
198
+ } else if (result.error) {
199
+ ctx.ui.notify(`Failed to stop remote auto-mode: ${result.error}`, "error");
200
+ } else {
201
+ ctx.ui.notify("Auto-mode is not running.", "info");
202
+ }
182
203
  return;
183
204
  }
184
205
  await stopAuto(ctx, pi);
@@ -199,42 +220,42 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
199
220
  }
200
221
 
201
222
  if (trimmed === "history" || trimmed.startsWith("history ")) {
202
- await handleHistory(trimmed.replace(/^history\s*/, "").trim(), ctx, process.cwd());
223
+ await handleHistory(trimmed.replace(/^history\s*/, "").trim(), ctx, projectRoot());
203
224
  return;
204
225
  }
205
226
 
206
227
  if (trimmed === "undo" || trimmed.startsWith("undo ")) {
207
- await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi, process.cwd());
228
+ await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi, projectRoot());
208
229
  return;
209
230
  }
210
231
 
211
232
  if (trimmed.startsWith("skip ")) {
212
- await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, process.cwd());
233
+ await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, projectRoot());
213
234
  return;
214
235
  }
215
236
 
216
237
  if (trimmed === "export" || trimmed.startsWith("export ")) {
217
- await handleExport(trimmed.replace(/^export\s*/, "").trim(), ctx, process.cwd());
238
+ await handleExport(trimmed.replace(/^export\s*/, "").trim(), ctx, projectRoot());
218
239
  return;
219
240
  }
220
241
 
221
242
  if (trimmed === "cleanup branches") {
222
- await handleCleanupBranches(ctx, process.cwd());
243
+ await handleCleanupBranches(ctx, projectRoot());
223
244
  return;
224
245
  }
225
246
 
226
247
  if (trimmed === "cleanup snapshots") {
227
- await handleCleanupSnapshots(ctx, process.cwd());
248
+ await handleCleanupSnapshots(ctx, projectRoot());
228
249
  return;
229
250
  }
230
251
 
231
252
  if (trimmed === "queue") {
232
- await showQueue(ctx, pi, process.cwd());
253
+ await showQueue(ctx, pi, projectRoot());
233
254
  return;
234
255
  }
235
256
 
236
257
  if (trimmed === "discuss") {
237
- await showDiscuss(ctx, pi, process.cwd());
258
+ await showDiscuss(ctx, pi, projectRoot());
238
259
  return;
239
260
  }
240
261
 
@@ -258,6 +279,15 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
258
279
  return;
259
280
  }
260
281
 
282
+ if (trimmed.startsWith("knowledge ")) {
283
+ await handleKnowledge(trimmed.replace(/^knowledge\s+/, "").trim(), ctx);
284
+ return;
285
+ }
286
+ if (trimmed === "knowledge") {
287
+ ctx.ui.notify("Usage: /gsd knowledge <rule|pattern|lesson> <description>. Example: /gsd knowledge rule Use real DB for integration tests", "warning");
288
+ return;
289
+ }
290
+
261
291
  if (trimmed === "migrate" || trimmed.startsWith("migrate ")) {
262
292
  const { handleMigrate } = await import("./migrate/command.js");
263
293
  await handleMigrate(trimmed.replace(/^migrate\s*/, "").trim(), ctx, pi);
@@ -271,12 +301,12 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
271
301
 
272
302
  if (trimmed === "") {
273
303
  // Bare /gsd defaults to step mode
274
- await startAuto(ctx, pi, process.cwd(), false, { step: true });
304
+ await startAuto(ctx, pi, projectRoot(), false, { step: true });
275
305
  return;
276
306
  }
277
307
 
278
308
  ctx.ui.notify(
279
- `Unknown: /gsd ${trimmed}. Use /gsd next|auto|stop|pause|status|queue|discuss|history|undo|skip <unit>|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer <change>.`,
309
+ `Unknown: /gsd ${trimmed}. Use /gsd next|auto|stop|pause|status|queue|discuss|history|undo|skip <unit>|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer <change>|knowledge <type> <entry>.`,
280
310
  "warning",
281
311
  );
282
312
  },
@@ -284,7 +314,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
284
314
  }
285
315
 
286
316
  async function handleStatus(ctx: ExtensionCommandContext): Promise<void> {
287
- const basePath = process.cwd();
317
+ const basePath = projectRoot();
288
318
  const state = await deriveState(basePath);
289
319
 
290
320
  if (state.registry.length === 0) {
@@ -368,9 +398,9 @@ async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: Exte
368
398
  const parts = trimmed ? trimmed.split(/\s+/) : [];
369
399
  const mode = parts[0] === "fix" || parts[0] === "heal" || parts[0] === "audit" ? parts[0] : "doctor";
370
400
  const requestedScope = mode === "doctor" ? parts[0] : parts[1];
371
- const scope = await selectDoctorScope(process.cwd(), requestedScope);
401
+ const scope = await selectDoctorScope(projectRoot(), requestedScope);
372
402
  const effectiveScope = mode === "audit" ? requestedScope : scope;
373
- const report = await runGSDDoctor(process.cwd(), {
403
+ const report = await runGSDDoctor(projectRoot(), {
374
404
  fix: mode === "fix" || mode === "heal",
375
405
  scope: effectiveScope,
376
406
  });
@@ -487,8 +517,10 @@ async function handlePrefsWizard(
487
517
  prefs.auto_supervisor = autoSup;
488
518
  }
489
519
 
490
- // ─── Git main branch ────────────────────────────────────────────────────
520
+ // ─── Git settings ───────────────────────────────────────────────────────
491
521
  const git: Record<string, unknown> = (prefs.git as Record<string, unknown>) ?? {};
522
+
523
+ // main_branch
492
524
  const currentBranch = git.main_branch ? String(git.main_branch) : "";
493
525
  const branchInput = await ctx.ui.input(
494
526
  `Git main branch${currentBranch ? ` (current: ${currentBranch})` : ""}:`,
@@ -502,6 +534,100 @@ async function handlePrefsWizard(
502
534
  delete git.main_branch;
503
535
  }
504
536
  }
537
+
538
+ // Boolean git toggles
539
+ const gitBooleanFields = [
540
+ { key: "auto_push", label: "Auto-push commits after committing", defaultVal: false },
541
+ { key: "push_branches", label: "Push milestone branches to remote", defaultVal: false },
542
+ { key: "snapshots", label: "Create WIP snapshot commits during long tasks", defaultVal: false },
543
+ ] as const;
544
+
545
+ for (const field of gitBooleanFields) {
546
+ const current = git[field.key];
547
+ const currentStr = current !== undefined ? String(current) : "";
548
+ const choice = await ctx.ui.select(
549
+ `${field.label}${currentStr ? ` (current: ${currentStr})` : ` (default: ${field.defaultVal})`}:`,
550
+ ["true", "false", "(keep current)"],
551
+ );
552
+ if (choice && choice !== "(keep current)") {
553
+ git[field.key] = choice === "true";
554
+ }
555
+ }
556
+
557
+ // remote
558
+ const currentRemote = git.remote ? String(git.remote) : "";
559
+ const remoteInput = await ctx.ui.input(
560
+ `Git remote name${currentRemote ? ` (current: ${currentRemote})` : " (default: origin)"}:`,
561
+ currentRemote || "origin",
562
+ );
563
+ if (remoteInput !== null && remoteInput !== undefined) {
564
+ const val = remoteInput.trim();
565
+ if (val && val !== "origin") {
566
+ git.remote = val;
567
+ } else if (!val && currentRemote) {
568
+ delete git.remote;
569
+ }
570
+ }
571
+
572
+ // pre_merge_check
573
+ const currentPreMerge = git.pre_merge_check !== undefined ? String(git.pre_merge_check) : "";
574
+ const preMergeChoice = await ctx.ui.select(
575
+ `Pre-merge check${currentPreMerge ? ` (current: ${currentPreMerge})` : " (default: false)"}:`,
576
+ ["true", "false", "auto", "(keep current)"],
577
+ );
578
+ if (preMergeChoice && preMergeChoice !== "(keep current)") {
579
+ if (preMergeChoice === "auto") {
580
+ git.pre_merge_check = "auto";
581
+ } else {
582
+ git.pre_merge_check = preMergeChoice === "true";
583
+ }
584
+ }
585
+
586
+ // commit_type
587
+ const currentCommitType = git.commit_type ? String(git.commit_type) : "";
588
+ const commitTypes = ["feat", "fix", "refactor", "docs", "test", "chore", "perf", "ci", "build", "style", "(inferred — default)", "(keep current)"];
589
+ const commitChoice = await ctx.ui.select(
590
+ `Default commit type${currentCommitType ? ` (current: ${currentCommitType})` : ""}:`,
591
+ commitTypes,
592
+ );
593
+ if (commitChoice && typeof commitChoice === "string" && commitChoice !== "(keep current)") {
594
+ if ((commitChoice as string).startsWith("(inferred")) {
595
+ delete git.commit_type;
596
+ } else {
597
+ git.commit_type = commitChoice;
598
+ }
599
+ }
600
+
601
+ // merge_strategy
602
+ const currentMerge = git.merge_strategy ? String(git.merge_strategy) : "";
603
+ const mergeChoice = await ctx.ui.select(
604
+ `Merge strategy${currentMerge ? ` (current: ${currentMerge})` : ""}:`,
605
+ ["squash", "merge", "(keep current)"],
606
+ );
607
+ if (mergeChoice && mergeChoice !== "(keep current)") {
608
+ git.merge_strategy = mergeChoice;
609
+ }
610
+
611
+ // isolation
612
+ const currentIsolation = git.isolation ? String(git.isolation) : "";
613
+ const isolationChoice = await ctx.ui.select(
614
+ `Git isolation strategy${currentIsolation ? ` (current: ${currentIsolation})` : " (default: worktree)"}:`,
615
+ ["worktree", "branch", "(keep current)"],
616
+ );
617
+ if (isolationChoice && isolationChoice !== "(keep current)") {
618
+ git.isolation = isolationChoice;
619
+ }
620
+
621
+ // ─── Git commit_docs ────────────────────────────────────────────────────
622
+ const currentCommitDocs = git.commit_docs;
623
+ const commitDocsChoice = await ctx.ui.select(
624
+ `Track .gsd/ planning docs in git${currentCommitDocs !== undefined ? ` (current: ${currentCommitDocs})` : ""}:`,
625
+ ["true", "false", "(keep current)"],
626
+ );
627
+ if (commitDocsChoice && commitDocsChoice !== "(keep current)") {
628
+ git.commit_docs = commitDocsChoice === "true";
629
+ }
630
+
505
631
  if (Object.keys(git).length > 0) {
506
632
  prefs.git = git;
507
633
  }
@@ -526,6 +652,89 @@ async function handlePrefsWizard(
526
652
  prefs.unique_milestone_ids = uniqueChoice === "true";
527
653
  }
528
654
 
655
+ // ─── Budget & cost control ────────────────────────────────────────────
656
+ const currentCeiling = prefs.budget_ceiling;
657
+ const ceilingStr = currentCeiling !== undefined ? String(currentCeiling) : "";
658
+ const ceilingInput = await ctx.ui.input(
659
+ `Budget ceiling (USD)${ceilingStr ? ` (current: $${ceilingStr})` : " (default: no limit)"}:`,
660
+ ceilingStr || "",
661
+ );
662
+ if (ceilingInput !== null && ceilingInput !== undefined) {
663
+ const val = ceilingInput.trim().replace(/^\$/, "");
664
+ if (val && !isNaN(Number(val)) && isFinite(Number(val))) {
665
+ prefs.budget_ceiling = Number(val);
666
+ } else if (val && (isNaN(Number(val)) || !isFinite(Number(val)))) {
667
+ ctx.ui.notify(`Invalid budget ceiling "${val}" — must be a number. Keeping previous value.`, "warning");
668
+ } else if (!val && ceilingStr) {
669
+ delete prefs.budget_ceiling;
670
+ }
671
+ }
672
+
673
+ const currentEnforcement = (prefs.budget_enforcement as string) ?? "";
674
+ const enforcementChoice = await ctx.ui.select(
675
+ `Budget enforcement${currentEnforcement ? ` (current: ${currentEnforcement})` : " (default: pause)"}:`,
676
+ ["warn", "pause", "halt", "(keep current)"],
677
+ );
678
+ if (enforcementChoice && enforcementChoice !== "(keep current)") {
679
+ prefs.budget_enforcement = enforcementChoice;
680
+ }
681
+
682
+ const currentContextPause = prefs.context_pause_threshold;
683
+ const contextPauseStr = currentContextPause !== undefined ? String(currentContextPause) : "";
684
+ const contextPauseInput = await ctx.ui.input(
685
+ `Context pause threshold (0-100%, 0=disabled)${contextPauseStr ? ` (current: ${contextPauseStr}%)` : " (default: 0)"}:`,
686
+ contextPauseStr || "0",
687
+ );
688
+ if (contextPauseInput !== null && contextPauseInput !== undefined) {
689
+ const val = contextPauseInput.trim().replace(/%$/, "");
690
+ if (val && !isNaN(Number(val)) && Number(val) >= 0 && Number(val) <= 100) {
691
+ const num = Number(val);
692
+ if (num === 0) {
693
+ delete prefs.context_pause_threshold;
694
+ } else {
695
+ prefs.context_pause_threshold = num;
696
+ }
697
+ } else if (val && (isNaN(Number(val)) || Number(val) < 0 || Number(val) > 100)) {
698
+ ctx.ui.notify(`Invalid context pause threshold "${val}" — must be 0-100. Keeping previous value.`, "warning");
699
+ }
700
+ }
701
+
702
+ // ─── Notifications ────────────────────────────────────────────────────
703
+ const notif: Record<string, boolean> = (prefs.notifications as Record<string, boolean>) ?? {};
704
+ const notifFields = [
705
+ { key: "enabled", label: "Notifications enabled (master toggle)", defaultVal: true },
706
+ { key: "on_complete", label: "Notify on unit completion", defaultVal: true },
707
+ { key: "on_error", label: "Notify on errors", defaultVal: true },
708
+ { key: "on_budget", label: "Notify on budget thresholds", defaultVal: true },
709
+ { key: "on_milestone", label: "Notify on milestone completion", defaultVal: true },
710
+ { key: "on_attention", label: "Notify when manual attention needed", defaultVal: true },
711
+ ] as const;
712
+
713
+ for (const field of notifFields) {
714
+ const current = notif[field.key];
715
+ const currentStr = current !== undefined ? String(current) : "";
716
+ const choice = await ctx.ui.select(
717
+ `${field.label}${currentStr ? ` (current: ${currentStr})` : ` (default: ${field.defaultVal})`}:`,
718
+ ["true", "false", "(keep current)"],
719
+ );
720
+ if (choice && choice !== "(keep current)") {
721
+ notif[field.key] = choice === "true";
722
+ }
723
+ }
724
+ if (Object.keys(notif).length > 0) {
725
+ prefs.notifications = notif;
726
+ }
727
+
728
+ // ─── UAT dispatch ─────────────────────────────────────────────────────
729
+ const currentUat = prefs.uat_dispatch;
730
+ const uatChoice = await ctx.ui.select(
731
+ `UAT dispatch mode${currentUat !== undefined ? ` (current: ${currentUat})` : " (default: false)"}:`,
732
+ ["true", "false", "(keep current)"],
733
+ );
734
+ if (uatChoice && uatChoice !== "(keep current)") {
735
+ prefs.uat_dispatch = uatChoice === "true";
736
+ }
737
+
529
738
  // ─── Serialize to frontmatter ───────────────────────────────────────────
530
739
  prefs.version = prefs.version || 1;
531
740
  const frontmatter = serializePreferencesToFrontmatter(prefs);
@@ -616,7 +825,10 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
616
825
  const orderedKeys = [
617
826
  "version", "always_use_skills", "prefer_skills", "avoid_skills",
618
827
  "skill_rules", "custom_instructions", "models", "skill_discovery",
619
- "auto_supervisor", "uat_dispatch", "unique_milestone_ids", "budget_ceiling", "remote_questions", "git",
828
+ "auto_supervisor", "uat_dispatch", "unique_milestone_ids",
829
+ "budget_ceiling", "budget_enforcement", "context_pause_threshold",
830
+ "notifications", "remote_questions", "git",
831
+ "post_unit_hooks", "pre_dispatch_hooks",
620
832
  ];
621
833
 
622
834
  const seen = new Set<string>();
@@ -954,6 +1166,35 @@ async function handleCleanupSnapshots(ctx: ExtensionCommandContext, basePath: st
954
1166
  ctx.ui.notify(`Pruned ${pruned} old snapshot refs. ${refs.length - pruned} remain.`, "success");
955
1167
  }
956
1168
 
1169
+ async function handleKnowledge(args: string, ctx: ExtensionCommandContext): Promise<void> {
1170
+ const parts = args.split(/\s+/);
1171
+ const typeArg = parts[0]?.toLowerCase();
1172
+
1173
+ if (!typeArg || !["rule", "pattern", "lesson"].includes(typeArg)) {
1174
+ ctx.ui.notify(
1175
+ "Usage: /gsd knowledge <rule|pattern|lesson> <description>\nExample: /gsd knowledge rule Use real DB for integration tests",
1176
+ "warning",
1177
+ );
1178
+ return;
1179
+ }
1180
+
1181
+ const entryText = parts.slice(1).join(" ").trim();
1182
+ if (!entryText) {
1183
+ ctx.ui.notify(`Usage: /gsd knowledge ${typeArg} <description>`, "warning");
1184
+ return;
1185
+ }
1186
+
1187
+ const type = typeArg as "rule" | "pattern" | "lesson";
1188
+ const basePath = process.cwd();
1189
+ const state = await deriveState(basePath);
1190
+ const scope = state.activeMilestone?.id
1191
+ ? `${state.activeMilestone.id}${state.activeSlice ? `/${state.activeSlice.id}` : ""}`
1192
+ : "global";
1193
+
1194
+ await appendKnowledge(basePath, type, entryText, scope);
1195
+ ctx.ui.notify(`Added ${type} to KNOWLEDGE.md: "${entryText}"`, "success");
1196
+ }
1197
+
957
1198
  async function handleSteer(change: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
958
1199
  const basePath = process.cwd();
959
1200
  const state = await deriveState(basePath);
@@ -0,0 +1,236 @@
1
+ /**
2
+ * GSD Task Complexity Classification
3
+ *
4
+ * Classifies task plans and unit types by complexity to enable model routing.
5
+ * Pure heuristics + adaptive learning — no LLM calls, sub-millisecond.
6
+ *
7
+ * Combined approach:
8
+ * - Task plan analysis (step count, file count, description length, signal words)
9
+ * - Unit type defaults (complete-slice → light, replan → heavy, etc.)
10
+ * - Budget pressure thresholds (50/75/90% graduated downgrade)
11
+ * - Adaptive learning via routing-history (optional)
12
+ *
13
+ * Classification output uses our TokenProfile-aligned TaskComplexity type
14
+ * for the simple classifier, and ComplexityTier for the full unit classifier.
15
+ */
16
+
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import type { ComplexityTier, ClassificationResult, TaskMetadata } from "./types.js";
20
+
21
+ // Re-export for convenience
22
+ export type { ComplexityTier, ClassificationResult, TaskMetadata };
23
+
24
+ // ─── Simple Task Complexity (for task plan analysis) ──────────────────────
25
+
26
+ export type TaskComplexity = "simple" | "standard" | "complex";
27
+
28
+ /** Words that signal non-trivial work requiring full reasoning capacity */
29
+ const COMPLEXITY_SIGNALS = [
30
+ "research", "investigate", "refactor", "migrate", "integrate",
31
+ "complex", "architect", "redesign", "security", "performance",
32
+ "concurrent", "parallel", "distributed", "backward.?compat",
33
+ "migration", "architecture", "concurrency", "compatibility",
34
+ ];
35
+ const COMPLEXITY_PATTERN = new RegExp(COMPLEXITY_SIGNALS.join("|"), "i");
36
+
37
+ /**
38
+ * Classify a task plan by its structural complexity.
39
+ * Used by dispatch to select execution_simple vs execution model.
40
+ */
41
+ export function classifyTaskComplexity(planContent: string): TaskComplexity {
42
+ if (!planContent || planContent.trim().length === 0) return "standard";
43
+
44
+ const stepsMatch = planContent.match(/##\s*Steps\s*\n([\s\S]*?)(?=\n##|\n---|$)/i);
45
+ const stepsSection = stepsMatch?.[1] ?? "";
46
+ const stepCount = (stepsSection.match(/^\s*\d+\.\s/gm) ?? []).length;
47
+
48
+ if (!stepsMatch) return "standard";
49
+
50
+ const stepsIdx = planContent.search(/##\s*Steps/i);
51
+ const descriptionLength = stepsIdx > 0 ? planContent.slice(0, stepsIdx).length : planContent.length;
52
+
53
+ const filePatterns = planContent.match(/`[a-zA-Z0-9_/.-]+\.[a-z]{1,4}`/g) ?? [];
54
+ const uniqueFiles = new Set(filePatterns.map(f => f.replace(/`/g, "")));
55
+ const fileCount = uniqueFiles.size;
56
+
57
+ const hasComplexitySignals = COMPLEXITY_PATTERN.test(planContent);
58
+
59
+ // Count fenced code blocks (from #579 Phase 4)
60
+ const codeBlockCount = (planContent.match(/^```/gm) ?? []).length / 2;
61
+
62
+ if (stepCount >= 8 || fileCount >= 8 || descriptionLength > 2000 || codeBlockCount >= 5) {
63
+ return "complex";
64
+ }
65
+
66
+ if (stepCount <= 3 && descriptionLength < 500 && fileCount <= 3 && !hasComplexitySignals) {
67
+ return "simple";
68
+ }
69
+
70
+ return "standard";
71
+ }
72
+
73
+ // ─── Unit Type → Default Tier Mapping (from #579) ─────────────────────────
74
+
75
+ const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
76
+ // Light: structured summaries, completion, UAT
77
+ "complete-slice": "light",
78
+ "run-uat": "light",
79
+
80
+ // Standard: research, routine planning
81
+ "research-milestone": "standard",
82
+ "research-slice": "standard",
83
+ "plan-milestone": "standard",
84
+ "plan-slice": "standard",
85
+
86
+ // Heavy: execution default (upgraded by metadata), replanning
87
+ "execute-task": "standard",
88
+ "replan-slice": "heavy",
89
+ "reassess-roadmap": "heavy",
90
+ "complete-milestone": "standard",
91
+ };
92
+
93
+ /**
94
+ * Classify unit complexity for model routing.
95
+ * Uses unit type defaults, task metadata analysis, and budget pressure.
96
+ *
97
+ * @param unitType The type of unit being dispatched
98
+ * @param unitId The unit ID (e.g. "M001/S01/T01")
99
+ * @param basePath Project base path (for reading task plans)
100
+ * @param budgetPct Current budget usage as fraction (0.0-1.0+), or undefined
101
+ * @param metadata Optional pre-parsed task metadata
102
+ */
103
+ export function classifyUnitComplexity(
104
+ unitType: string,
105
+ unitId: string,
106
+ basePath: string,
107
+ budgetPct?: number,
108
+ metadata?: TaskMetadata,
109
+ ): ClassificationResult {
110
+ // Hook units default to light
111
+ if (unitType.startsWith("hook/")) {
112
+ return applyBudgetPressure({ tier: "light", reason: "hook unit", downgraded: false }, budgetPct);
113
+ }
114
+
115
+ // Triage/capture units default to light
116
+ if (unitType === "triage-captures" || unitType.startsWith("quick-task")) {
117
+ return applyBudgetPressure({ tier: "light", reason: `${unitType} unit`, downgraded: false }, budgetPct);
118
+ }
119
+
120
+ let tier = UNIT_TYPE_TIERS[unitType] ?? "standard";
121
+ let reason = `unit type: ${unitType}`;
122
+
123
+ // For execute-task, analyze task metadata for complexity signals
124
+ if (unitType === "execute-task") {
125
+ const analysis = analyzeTaskFromPlan(unitId, basePath, metadata);
126
+ if (analysis) {
127
+ tier = analysis.tier;
128
+ reason = analysis.reason;
129
+ }
130
+ }
131
+
132
+ return applyBudgetPressure({ tier, reason, downgraded: false }, budgetPct);
133
+ }
134
+
135
+ // ─── Tier Helpers ─────────────────────────────────────────────────────────
136
+
137
+ export function tierLabel(tier: ComplexityTier): string {
138
+ switch (tier) {
139
+ case "light": return "L";
140
+ case "standard": return "S";
141
+ case "heavy": return "H";
142
+ }
143
+ }
144
+
145
+ export function tierOrdinal(tier: ComplexityTier): number {
146
+ switch (tier) {
147
+ case "light": return 0;
148
+ case "standard": return 1;
149
+ case "heavy": return 2;
150
+ }
151
+ }
152
+
153
+ export function escalateTier(currentTier: ComplexityTier): ComplexityTier | null {
154
+ switch (currentTier) {
155
+ case "light": return "standard";
156
+ case "standard": return "heavy";
157
+ case "heavy": return null;
158
+ }
159
+ }
160
+
161
+ // ─── Budget Pressure (from #579 — graduated thresholds) ───────────────────
162
+
163
+ function applyBudgetPressure(
164
+ result: ClassificationResult,
165
+ budgetPct?: number,
166
+ ): ClassificationResult {
167
+ if (budgetPct === undefined || budgetPct < 0.5) return result;
168
+
169
+ const original = result.tier;
170
+
171
+ if (budgetPct >= 0.9) {
172
+ // >90%: almost everything goes to light
173
+ if (result.tier !== "heavy") {
174
+ result.tier = "light";
175
+ } else {
176
+ result.tier = "standard";
177
+ }
178
+ } else if (budgetPct >= 0.75) {
179
+ // 75-90%: only heavy stays, standard → light
180
+ if (result.tier === "standard") {
181
+ result.tier = "light";
182
+ }
183
+ } else {
184
+ // 50-75%: standard → light
185
+ if (result.tier === "standard") {
186
+ result.tier = "light";
187
+ }
188
+ }
189
+
190
+ if (result.tier !== original) {
191
+ result.downgraded = true;
192
+ result.reason = `${result.reason} (budget pressure: ${Math.round(budgetPct * 100)}%)`;
193
+ }
194
+
195
+ return result;
196
+ }
197
+
198
+ // ─── Task Plan Analysis ───────────────────────────────────────────────────
199
+
200
+ interface TaskAnalysis {
201
+ tier: ComplexityTier;
202
+ reason: string;
203
+ }
204
+
205
+ function analyzeTaskFromPlan(
206
+ unitId: string,
207
+ basePath: string,
208
+ metadata?: TaskMetadata,
209
+ ): TaskAnalysis | null {
210
+ // Try to read the task plan for analysis
211
+ const parts = unitId.split("/");
212
+ if (parts.length < 3) return null;
213
+
214
+ const [mid, sid, tid] = parts;
215
+ const planPath = join(basePath, ".gsd", "milestones", mid, "slices", sid, "tasks", `${tid}-PLAN.md`);
216
+
217
+ let planContent = "";
218
+ try {
219
+ if (existsSync(planPath)) {
220
+ planContent = readFileSync(planPath, "utf-8");
221
+ }
222
+ } catch {
223
+ return null;
224
+ }
225
+
226
+ if (!planContent) return null;
227
+
228
+ const taskComplexity = classifyTaskComplexity(planContent);
229
+
230
+ // Map TaskComplexity to ComplexityTier
231
+ switch (taskComplexity) {
232
+ case "simple": return { tier: "light", reason: "task plan: simple (few steps, small scope)" };
233
+ case "complex": return { tier: "heavy", reason: "task plan: complex (many steps/files or signal words)" };
234
+ default: return { tier: "standard", reason: "task plan: standard complexity" };
235
+ }
236
+ }