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
@@ -11,8 +11,11 @@ import { join, dirname } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { deriveState } from "./state.js";
13
13
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
14
+ import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
14
15
  import { showQueue, showDiscuss } from "./guided-flow.js";
15
16
  import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
17
+ import { resolveProjectRoot } from "./worktree.js";
18
+ import { appendCapture, hasPendingCaptures, loadPendingCaptures } from "./captures.js";
16
19
  import {
17
20
  getGlobalGSDPreferencesPath,
18
21
  getLegacyGlobalGSDPreferencesPath,
@@ -22,7 +25,7 @@ import {
22
25
  loadEffectiveGSDPreferences,
23
26
  resolveAllSkillReferences,
24
27
  } from "./preferences.js";
25
- import { loadFile, saveFile, appendOverride } from "./files.js";
28
+ import { loadFile, saveFile, appendOverride, appendKnowledge } from "./files.js";
26
29
  import {
27
30
  formatDoctorIssuesForPrompt,
28
31
  formatDoctorReport,
@@ -56,14 +59,20 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
56
59
  );
57
60
  }
58
61
 
62
+ /** Resolve the effective project root, accounting for worktree paths. */
63
+ function projectRoot(): string {
64
+ return resolveProjectRoot(process.cwd());
65
+ }
66
+
59
67
  export function registerGSDCommand(pi: ExtensionAPI): void {
60
68
  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",
69
+ description: "GSD — Get Shit Done: /gsd next|auto|stop|pause|status|visualize|queue|capture|triage|history|undo|skip|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer|knowledge",
62
70
  getArgumentCompletions: (prefix: string) => {
63
71
  const subcommands = [
64
- "next", "auto", "stop", "pause", "status", "queue", "discuss",
72
+ "next", "auto", "stop", "pause", "status", "visualize", "queue", "discuss",
73
+ "capture", "triage",
65
74
  "history", "undo", "skip", "export", "cleanup", "prefs",
66
- "config", "hooks", "doctor", "migrate", "remote", "steer",
75
+ "config", "hooks", "doctor", "migrate", "remote", "steer", "knowledge",
67
76
  ];
68
77
  const parts = prefix.trim().split(/\s+/);
69
78
 
@@ -126,6 +135,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
126
135
  .map((cmd) => ({ value: `cleanup ${cmd}`, label: cmd }));
127
136
  }
128
137
 
138
+ if (parts[0] === "knowledge" && parts.length <= 2) {
139
+ const subPrefix = parts[1] ?? "";
140
+ return ["rule", "pattern", "lesson"]
141
+ .filter((cmd) => cmd.startsWith(subPrefix))
142
+ .map((cmd) => ({ value: `knowledge ${cmd}`, label: cmd }));
143
+ }
144
+
129
145
  if (parts[0] === "doctor") {
130
146
  const modePrefix = parts[1] ?? "";
131
147
  const modes = ["fix", "heal", "audit"];
@@ -150,6 +166,11 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
150
166
  return;
151
167
  }
152
168
 
169
+ if (trimmed === "visualize") {
170
+ await handleVisualize(ctx);
171
+ return;
172
+ }
173
+
153
174
  if (trimmed === "prefs" || trimmed.startsWith("prefs ")) {
154
175
  await handlePrefs(trimmed.replace(/^prefs\s*/, "").trim(), ctx);
155
176
  return;
@@ -162,24 +183,24 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
162
183
 
163
184
  if (trimmed === "next" || trimmed.startsWith("next ")) {
164
185
  if (trimmed.includes("--dry-run")) {
165
- await handleDryRun(ctx, process.cwd());
186
+ await handleDryRun(ctx, projectRoot());
166
187
  return;
167
188
  }
168
189
  const verboseMode = trimmed.includes("--verbose");
169
- await startAuto(ctx, pi, process.cwd(), verboseMode, { step: true });
190
+ await startAuto(ctx, pi, projectRoot(), verboseMode, { step: true });
170
191
  return;
171
192
  }
172
193
 
173
194
  if (trimmed === "auto" || trimmed.startsWith("auto ")) {
174
195
  const verboseMode = trimmed.includes("--verbose");
175
- await startAuto(ctx, pi, process.cwd(), verboseMode);
196
+ await startAuto(ctx, pi, projectRoot(), verboseMode);
176
197
  return;
177
198
  }
178
199
 
179
200
  if (trimmed === "stop") {
180
201
  if (!isAutoActive() && !isAutoPaused()) {
181
202
  // Not running in this process — check for a remote auto-mode session
182
- const result = stopAutoRemote(process.cwd());
203
+ const result = stopAutoRemote(projectRoot());
183
204
  if (result.found) {
184
205
  ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
185
206
  } else if (result.error) {
@@ -207,42 +228,52 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
207
228
  }
208
229
 
209
230
  if (trimmed === "history" || trimmed.startsWith("history ")) {
210
- await handleHistory(trimmed.replace(/^history\s*/, "").trim(), ctx, process.cwd());
231
+ await handleHistory(trimmed.replace(/^history\s*/, "").trim(), ctx, projectRoot());
211
232
  return;
212
233
  }
213
234
 
214
235
  if (trimmed === "undo" || trimmed.startsWith("undo ")) {
215
- await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi, process.cwd());
236
+ await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi, projectRoot());
216
237
  return;
217
238
  }
218
239
 
219
240
  if (trimmed.startsWith("skip ")) {
220
- await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, process.cwd());
241
+ await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, projectRoot());
221
242
  return;
222
243
  }
223
244
 
224
245
  if (trimmed === "export" || trimmed.startsWith("export ")) {
225
- await handleExport(trimmed.replace(/^export\s*/, "").trim(), ctx, process.cwd());
246
+ await handleExport(trimmed.replace(/^export\s*/, "").trim(), ctx, projectRoot());
226
247
  return;
227
248
  }
228
249
 
229
250
  if (trimmed === "cleanup branches") {
230
- await handleCleanupBranches(ctx, process.cwd());
251
+ await handleCleanupBranches(ctx, projectRoot());
231
252
  return;
232
253
  }
233
254
 
234
255
  if (trimmed === "cleanup snapshots") {
235
- await handleCleanupSnapshots(ctx, process.cwd());
256
+ await handleCleanupSnapshots(ctx, projectRoot());
236
257
  return;
237
258
  }
238
259
 
239
260
  if (trimmed === "queue") {
240
- await showQueue(ctx, pi, process.cwd());
261
+ await showQueue(ctx, pi, projectRoot());
241
262
  return;
242
263
  }
243
264
 
244
265
  if (trimmed === "discuss") {
245
- await showDiscuss(ctx, pi, process.cwd());
266
+ await showDiscuss(ctx, pi, projectRoot());
267
+ return;
268
+ }
269
+
270
+ if (trimmed.startsWith("capture ") || trimmed === "capture") {
271
+ await handleCapture(trimmed.replace(/^capture\s*/, "").trim(), ctx);
272
+ return;
273
+ }
274
+
275
+ if (trimmed === "triage") {
276
+ await handleTriage(ctx, pi, process.cwd());
246
277
  return;
247
278
  }
248
279
 
@@ -266,6 +297,15 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
266
297
  return;
267
298
  }
268
299
 
300
+ if (trimmed.startsWith("knowledge ")) {
301
+ await handleKnowledge(trimmed.replace(/^knowledge\s+/, "").trim(), ctx);
302
+ return;
303
+ }
304
+ if (trimmed === "knowledge") {
305
+ ctx.ui.notify("Usage: /gsd knowledge <rule|pattern|lesson> <description>. Example: /gsd knowledge rule Use real DB for integration tests", "warning");
306
+ return;
307
+ }
308
+
269
309
  if (trimmed === "migrate" || trimmed.startsWith("migrate ")) {
270
310
  const { handleMigrate } = await import("./migrate/command.js");
271
311
  await handleMigrate(trimmed.replace(/^migrate\s*/, "").trim(), ctx, pi);
@@ -279,12 +319,12 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
279
319
 
280
320
  if (trimmed === "") {
281
321
  // Bare /gsd defaults to step mode
282
- await startAuto(ctx, pi, process.cwd(), false, { step: true });
322
+ await startAuto(ctx, pi, projectRoot(), false, { step: true });
283
323
  return;
284
324
  }
285
325
 
286
326
  ctx.ui.notify(
287
- `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>.`,
327
+ `Unknown: /gsd ${trimmed}. Use /gsd next|auto|stop|pause|status|visualize|queue|capture|triage|discuss|history|undo|skip <unit>|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer <change>|knowledge <type> <entry>.`,
288
328
  "warning",
289
329
  );
290
330
  },
@@ -292,7 +332,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
292
332
  }
293
333
 
294
334
  async function handleStatus(ctx: ExtensionCommandContext): Promise<void> {
295
- const basePath = process.cwd();
335
+ const basePath = projectRoot();
296
336
  const state = await deriveState(basePath);
297
337
 
298
338
  if (state.registry.length === 0) {
@@ -322,6 +362,28 @@ export async function fireStatusViaCommand(
322
362
  await handleStatus(ctx as ExtensionCommandContext);
323
363
  }
324
364
 
365
+ async function handleVisualize(ctx: ExtensionCommandContext): Promise<void> {
366
+ if (!ctx.hasUI) {
367
+ ctx.ui.notify("Visualizer requires an interactive terminal.", "warning");
368
+ return;
369
+ }
370
+
371
+ await ctx.ui.custom<void>(
372
+ (tui, theme, _kb, done) => {
373
+ return new GSDVisualizerOverlay(tui, theme, () => done());
374
+ },
375
+ {
376
+ overlay: true,
377
+ overlayOptions: {
378
+ width: "80%",
379
+ minWidth: 80,
380
+ maxHeight: "90%",
381
+ anchor: "center",
382
+ },
383
+ },
384
+ );
385
+ }
386
+
325
387
  async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<void> {
326
388
  const trimmed = args.trim();
327
389
 
@@ -376,9 +438,9 @@ async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: Exte
376
438
  const parts = trimmed ? trimmed.split(/\s+/) : [];
377
439
  const mode = parts[0] === "fix" || parts[0] === "heal" || parts[0] === "audit" ? parts[0] : "doctor";
378
440
  const requestedScope = mode === "doctor" ? parts[0] : parts[1];
379
- const scope = await selectDoctorScope(process.cwd(), requestedScope);
441
+ const scope = await selectDoctorScope(projectRoot(), requestedScope);
380
442
  const effectiveScope = mode === "audit" ? requestedScope : scope;
381
- const report = await runGSDDoctor(process.cwd(), {
443
+ const report = await runGSDDoctor(projectRoot(), {
382
444
  fix: mode === "fix" || mode === "heal",
383
445
  scope: effectiveScope,
384
446
  });
@@ -495,8 +557,10 @@ async function handlePrefsWizard(
495
557
  prefs.auto_supervisor = autoSup;
496
558
  }
497
559
 
498
- // ─── Git main branch ────────────────────────────────────────────────────
560
+ // ─── Git settings ───────────────────────────────────────────────────────
499
561
  const git: Record<string, unknown> = (prefs.git as Record<string, unknown>) ?? {};
562
+
563
+ // main_branch
500
564
  const currentBranch = git.main_branch ? String(git.main_branch) : "";
501
565
  const branchInput = await ctx.ui.input(
502
566
  `Git main branch${currentBranch ? ` (current: ${currentBranch})` : ""}:`,
@@ -510,6 +574,90 @@ async function handlePrefsWizard(
510
574
  delete git.main_branch;
511
575
  }
512
576
  }
577
+
578
+ // Boolean git toggles
579
+ const gitBooleanFields = [
580
+ { key: "auto_push", label: "Auto-push commits after committing", defaultVal: false },
581
+ { key: "push_branches", label: "Push milestone branches to remote", defaultVal: false },
582
+ { key: "snapshots", label: "Create WIP snapshot commits during long tasks", defaultVal: false },
583
+ ] as const;
584
+
585
+ for (const field of gitBooleanFields) {
586
+ const current = git[field.key];
587
+ const currentStr = current !== undefined ? String(current) : "";
588
+ const choice = await ctx.ui.select(
589
+ `${field.label}${currentStr ? ` (current: ${currentStr})` : ` (default: ${field.defaultVal})`}:`,
590
+ ["true", "false", "(keep current)"],
591
+ );
592
+ if (choice && choice !== "(keep current)") {
593
+ git[field.key] = choice === "true";
594
+ }
595
+ }
596
+
597
+ // remote
598
+ const currentRemote = git.remote ? String(git.remote) : "";
599
+ const remoteInput = await ctx.ui.input(
600
+ `Git remote name${currentRemote ? ` (current: ${currentRemote})` : " (default: origin)"}:`,
601
+ currentRemote || "origin",
602
+ );
603
+ if (remoteInput !== null && remoteInput !== undefined) {
604
+ const val = remoteInput.trim();
605
+ if (val && val !== "origin") {
606
+ git.remote = val;
607
+ } else if (!val && currentRemote) {
608
+ delete git.remote;
609
+ }
610
+ }
611
+
612
+ // pre_merge_check
613
+ const currentPreMerge = git.pre_merge_check !== undefined ? String(git.pre_merge_check) : "";
614
+ const preMergeChoice = await ctx.ui.select(
615
+ `Pre-merge check${currentPreMerge ? ` (current: ${currentPreMerge})` : " (default: false)"}:`,
616
+ ["true", "false", "auto", "(keep current)"],
617
+ );
618
+ if (preMergeChoice && preMergeChoice !== "(keep current)") {
619
+ if (preMergeChoice === "auto") {
620
+ git.pre_merge_check = "auto";
621
+ } else {
622
+ git.pre_merge_check = preMergeChoice === "true";
623
+ }
624
+ }
625
+
626
+ // commit_type
627
+ const currentCommitType = git.commit_type ? String(git.commit_type) : "";
628
+ const commitTypes = ["feat", "fix", "refactor", "docs", "test", "chore", "perf", "ci", "build", "style", "(inferred — default)", "(keep current)"];
629
+ const commitChoice = await ctx.ui.select(
630
+ `Default commit type${currentCommitType ? ` (current: ${currentCommitType})` : ""}:`,
631
+ commitTypes,
632
+ );
633
+ if (commitChoice && typeof commitChoice === "string" && commitChoice !== "(keep current)") {
634
+ if ((commitChoice as string).startsWith("(inferred")) {
635
+ delete git.commit_type;
636
+ } else {
637
+ git.commit_type = commitChoice;
638
+ }
639
+ }
640
+
641
+ // merge_strategy
642
+ const currentMerge = git.merge_strategy ? String(git.merge_strategy) : "";
643
+ const mergeChoice = await ctx.ui.select(
644
+ `Merge strategy${currentMerge ? ` (current: ${currentMerge})` : ""}:`,
645
+ ["squash", "merge", "(keep current)"],
646
+ );
647
+ if (mergeChoice && mergeChoice !== "(keep current)") {
648
+ git.merge_strategy = mergeChoice;
649
+ }
650
+
651
+ // isolation
652
+ const currentIsolation = git.isolation ? String(git.isolation) : "";
653
+ const isolationChoice = await ctx.ui.select(
654
+ `Git isolation strategy${currentIsolation ? ` (current: ${currentIsolation})` : " (default: worktree)"}:`,
655
+ ["worktree", "branch", "(keep current)"],
656
+ );
657
+ if (isolationChoice && isolationChoice !== "(keep current)") {
658
+ git.isolation = isolationChoice;
659
+ }
660
+
513
661
  // ─── Git commit_docs ────────────────────────────────────────────────────
514
662
  const currentCommitDocs = git.commit_docs;
515
663
  const commitDocsChoice = await ctx.ui.select(
@@ -544,6 +692,89 @@ async function handlePrefsWizard(
544
692
  prefs.unique_milestone_ids = uniqueChoice === "true";
545
693
  }
546
694
 
695
+ // ─── Budget & cost control ────────────────────────────────────────────
696
+ const currentCeiling = prefs.budget_ceiling;
697
+ const ceilingStr = currentCeiling !== undefined ? String(currentCeiling) : "";
698
+ const ceilingInput = await ctx.ui.input(
699
+ `Budget ceiling (USD)${ceilingStr ? ` (current: $${ceilingStr})` : " (default: no limit)"}:`,
700
+ ceilingStr || "",
701
+ );
702
+ if (ceilingInput !== null && ceilingInput !== undefined) {
703
+ const val = ceilingInput.trim().replace(/^\$/, "");
704
+ if (val && !isNaN(Number(val)) && isFinite(Number(val))) {
705
+ prefs.budget_ceiling = Number(val);
706
+ } else if (val && (isNaN(Number(val)) || !isFinite(Number(val)))) {
707
+ ctx.ui.notify(`Invalid budget ceiling "${val}" — must be a number. Keeping previous value.`, "warning");
708
+ } else if (!val && ceilingStr) {
709
+ delete prefs.budget_ceiling;
710
+ }
711
+ }
712
+
713
+ const currentEnforcement = (prefs.budget_enforcement as string) ?? "";
714
+ const enforcementChoice = await ctx.ui.select(
715
+ `Budget enforcement${currentEnforcement ? ` (current: ${currentEnforcement})` : " (default: pause)"}:`,
716
+ ["warn", "pause", "halt", "(keep current)"],
717
+ );
718
+ if (enforcementChoice && enforcementChoice !== "(keep current)") {
719
+ prefs.budget_enforcement = enforcementChoice;
720
+ }
721
+
722
+ const currentContextPause = prefs.context_pause_threshold;
723
+ const contextPauseStr = currentContextPause !== undefined ? String(currentContextPause) : "";
724
+ const contextPauseInput = await ctx.ui.input(
725
+ `Context pause threshold (0-100%, 0=disabled)${contextPauseStr ? ` (current: ${contextPauseStr}%)` : " (default: 0)"}:`,
726
+ contextPauseStr || "0",
727
+ );
728
+ if (contextPauseInput !== null && contextPauseInput !== undefined) {
729
+ const val = contextPauseInput.trim().replace(/%$/, "");
730
+ if (val && !isNaN(Number(val)) && Number(val) >= 0 && Number(val) <= 100) {
731
+ const num = Number(val);
732
+ if (num === 0) {
733
+ delete prefs.context_pause_threshold;
734
+ } else {
735
+ prefs.context_pause_threshold = num;
736
+ }
737
+ } else if (val && (isNaN(Number(val)) || Number(val) < 0 || Number(val) > 100)) {
738
+ ctx.ui.notify(`Invalid context pause threshold "${val}" — must be 0-100. Keeping previous value.`, "warning");
739
+ }
740
+ }
741
+
742
+ // ─── Notifications ────────────────────────────────────────────────────
743
+ const notif: Record<string, boolean> = (prefs.notifications as Record<string, boolean>) ?? {};
744
+ const notifFields = [
745
+ { key: "enabled", label: "Notifications enabled (master toggle)", defaultVal: true },
746
+ { key: "on_complete", label: "Notify on unit completion", defaultVal: true },
747
+ { key: "on_error", label: "Notify on errors", defaultVal: true },
748
+ { key: "on_budget", label: "Notify on budget thresholds", defaultVal: true },
749
+ { key: "on_milestone", label: "Notify on milestone completion", defaultVal: true },
750
+ { key: "on_attention", label: "Notify when manual attention needed", defaultVal: true },
751
+ ] as const;
752
+
753
+ for (const field of notifFields) {
754
+ const current = notif[field.key];
755
+ const currentStr = current !== undefined ? String(current) : "";
756
+ const choice = await ctx.ui.select(
757
+ `${field.label}${currentStr ? ` (current: ${currentStr})` : ` (default: ${field.defaultVal})`}:`,
758
+ ["true", "false", "(keep current)"],
759
+ );
760
+ if (choice && choice !== "(keep current)") {
761
+ notif[field.key] = choice === "true";
762
+ }
763
+ }
764
+ if (Object.keys(notif).length > 0) {
765
+ prefs.notifications = notif;
766
+ }
767
+
768
+ // ─── UAT dispatch ─────────────────────────────────────────────────────
769
+ const currentUat = prefs.uat_dispatch;
770
+ const uatChoice = await ctx.ui.select(
771
+ `UAT dispatch mode${currentUat !== undefined ? ` (current: ${currentUat})` : " (default: false)"}:`,
772
+ ["true", "false", "(keep current)"],
773
+ );
774
+ if (uatChoice && uatChoice !== "(keep current)") {
775
+ prefs.uat_dispatch = uatChoice === "true";
776
+ }
777
+
547
778
  // ─── Serialize to frontmatter ───────────────────────────────────────────
548
779
  prefs.version = prefs.version || 1;
549
780
  const frontmatter = serializePreferencesToFrontmatter(prefs);
@@ -634,7 +865,10 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
634
865
  const orderedKeys = [
635
866
  "version", "always_use_skills", "prefer_skills", "avoid_skills",
636
867
  "skill_rules", "custom_instructions", "models", "skill_discovery",
637
- "auto_supervisor", "uat_dispatch", "unique_milestone_ids", "budget_ceiling", "remote_questions", "git",
868
+ "auto_supervisor", "uat_dispatch", "unique_milestone_ids",
869
+ "budget_ceiling", "budget_enforcement", "context_pause_threshold",
870
+ "notifications", "remote_questions", "git",
871
+ "post_unit_hooks", "pre_dispatch_hooks",
638
872
  ];
639
873
 
640
874
  const seen = new Set<string>();
@@ -972,6 +1206,131 @@ async function handleCleanupSnapshots(ctx: ExtensionCommandContext, basePath: st
972
1206
  ctx.ui.notify(`Pruned ${pruned} old snapshot refs. ${refs.length - pruned} remain.`, "success");
973
1207
  }
974
1208
 
1209
+ async function handleKnowledge(args: string, ctx: ExtensionCommandContext): Promise<void> {
1210
+ const parts = args.split(/\s+/);
1211
+ const typeArg = parts[0]?.toLowerCase();
1212
+
1213
+ if (!typeArg || !["rule", "pattern", "lesson"].includes(typeArg)) {
1214
+ ctx.ui.notify(
1215
+ "Usage: /gsd knowledge <rule|pattern|lesson> <description>\nExample: /gsd knowledge rule Use real DB for integration tests",
1216
+ "warning",
1217
+ );
1218
+ return;
1219
+ }
1220
+
1221
+ const entryText = parts.slice(1).join(" ").trim();
1222
+ if (!entryText) {
1223
+ ctx.ui.notify(`Usage: /gsd knowledge ${typeArg} <description>`, "warning");
1224
+ return;
1225
+ }
1226
+
1227
+ const type = typeArg as "rule" | "pattern" | "lesson";
1228
+ const basePath = process.cwd();
1229
+ const state = await deriveState(basePath);
1230
+ const scope = state.activeMilestone?.id
1231
+ ? `${state.activeMilestone.id}${state.activeSlice ? `/${state.activeSlice.id}` : ""}`
1232
+ : "global";
1233
+
1234
+ await appendKnowledge(basePath, type, entryText, scope);
1235
+ ctx.ui.notify(`Added ${type} to KNOWLEDGE.md: "${entryText}"`, "success");
1236
+ }
1237
+
1238
+ // ─── Capture Command ──────────────────────────────────────────────────────────
1239
+
1240
+ /**
1241
+ * Handle `/gsd capture "..."` — fire-and-forget thought capture.
1242
+ * Appends to `.gsd/CAPTURES.md` without interrupting auto-mode.
1243
+ * Works in all modes: auto running, paused, stopped, no project.
1244
+ */
1245
+ async function handleCapture(args: string, ctx: ExtensionCommandContext): Promise<void> {
1246
+ // Strip surrounding quotes from the argument
1247
+ let text = args.trim();
1248
+ if (!text) {
1249
+ ctx.ui.notify('Usage: /gsd capture "your thought here"', "warning");
1250
+ return;
1251
+ }
1252
+ // Remove wrapping quotes (single or double)
1253
+ if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
1254
+ text = text.slice(1, -1);
1255
+ }
1256
+ if (!text) {
1257
+ ctx.ui.notify('Usage: /gsd capture "your thought here"', "warning");
1258
+ return;
1259
+ }
1260
+
1261
+ const basePath = process.cwd();
1262
+
1263
+ // Ensure .gsd/ exists — capture should work even without a milestone
1264
+ const gsdDir = join(basePath, ".gsd");
1265
+ if (!existsSync(gsdDir)) {
1266
+ mkdirSync(gsdDir, { recursive: true });
1267
+ }
1268
+
1269
+ const id = appendCapture(basePath, text);
1270
+ ctx.ui.notify(`Captured: ${id} — "${text.length > 60 ? text.slice(0, 57) + "..." : text}"`, "info");
1271
+ }
1272
+
1273
+ // ─── Triage Command ───────────────────────────────────────────────────────────
1274
+
1275
+ /**
1276
+ * Handle `/gsd triage` — manually trigger triage of pending captures.
1277
+ * Dispatches the triage prompt to the LLM for classification.
1278
+ * Triage result handling (confirmation UI) is wired in T03.
1279
+ */
1280
+ async function handleTriage(ctx: ExtensionCommandContext, pi: ExtensionAPI, basePath: string): Promise<void> {
1281
+ if (!hasPendingCaptures(basePath)) {
1282
+ ctx.ui.notify("No pending captures to triage.", "info");
1283
+ return;
1284
+ }
1285
+
1286
+ const pending = loadPendingCaptures(basePath);
1287
+ ctx.ui.notify(`Triaging ${pending.length} pending capture${pending.length === 1 ? "" : "s"}...`, "info");
1288
+
1289
+ // Build context for the triage prompt
1290
+ const state = await deriveState(basePath);
1291
+ let currentPlan = "";
1292
+ let roadmapContext = "";
1293
+
1294
+ if (state.activeMilestone && state.activeSlice) {
1295
+ const { resolveSliceFile, resolveMilestoneFile } = await import("./paths.js");
1296
+ const planFile = resolveSliceFile(basePath, state.activeMilestone.id, state.activeSlice.id, "PLAN");
1297
+ if (planFile) {
1298
+ const { loadFile: load } = await import("./files.js");
1299
+ currentPlan = (await load(planFile)) ?? "";
1300
+ }
1301
+ const roadmapFile = resolveMilestoneFile(basePath, state.activeMilestone.id, "ROADMAP");
1302
+ if (roadmapFile) {
1303
+ const { loadFile: load } = await import("./files.js");
1304
+ roadmapContext = (await load(roadmapFile)) ?? "";
1305
+ }
1306
+ }
1307
+
1308
+ // Format pending captures for the prompt
1309
+ const capturesList = pending.map(c =>
1310
+ `- **${c.id}**: "${c.text}" (captured: ${c.timestamp})`
1311
+ ).join("\n");
1312
+
1313
+ // Dispatch triage prompt
1314
+ const { loadPrompt } = await import("./prompt-loader.js");
1315
+ const prompt = loadPrompt("triage-captures", {
1316
+ pendingCaptures: capturesList,
1317
+ currentPlan: currentPlan || "(no active slice plan)",
1318
+ roadmapContext: roadmapContext || "(no active roadmap)",
1319
+ });
1320
+
1321
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
1322
+ const workflow = readFileSync(workflowPath, "utf-8");
1323
+
1324
+ pi.sendMessage(
1325
+ {
1326
+ customType: "gsd-triage",
1327
+ content: `Read the following GSD workflow protocol and execute exactly.\n\n${workflow}\n\n## Your Task\n\n${prompt}`,
1328
+ display: false,
1329
+ },
1330
+ { triggerTurn: true },
1331
+ );
1332
+ }
1333
+
975
1334
  async function handleSteer(change: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
976
1335
  const basePath = process.cwd();
977
1336
  const state = await deriveState(basePath);