gsd-pi 2.17.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 (153) 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-prompts.ts +20 -1
  7. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  8. package/dist/resources/extensions/gsd/auto.ts +123 -10
  9. package/dist/resources/extensions/gsd/commands.ts +245 -22
  10. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  11. package/dist/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  12. package/dist/resources/extensions/gsd/files.ts +123 -1
  13. package/dist/resources/extensions/gsd/guided-flow.ts +237 -4
  14. package/dist/resources/extensions/gsd/index.ts +47 -3
  15. package/dist/resources/extensions/gsd/paths.ts +9 -0
  16. package/dist/resources/extensions/gsd/preferences.ts +59 -1
  17. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  18. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  19. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  20. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  21. package/dist/resources/extensions/gsd/state.ts +15 -3
  22. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  23. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  24. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  25. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  26. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  27. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  28. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  29. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  30. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  31. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  32. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  33. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  34. package/dist/resources/extensions/gsd/worktree.ts +22 -0
  35. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  36. package/package.json +1 -1
  37. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  38. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  39. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  40. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  41. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  42. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  43. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  44. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  46. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  47. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  48. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  49. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  50. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  51. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  52. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  53. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  54. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  55. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  56. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  57. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  58. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  59. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  60. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  61. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  62. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  63. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  64. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  65. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  66. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  68. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  70. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  71. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  72. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  74. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  75. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  76. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  78. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  80. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  83. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  85. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/index.js +4 -1
  87. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/main.js +17 -2
  90. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  92. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  105. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  106. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  107. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  108. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  109. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  110. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  111. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  112. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  113. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  114. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  115. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  116. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  117. package/packages/pi-coding-agent/src/index.ts +5 -0
  118. package/packages/pi-coding-agent/src/main.ts +19 -2
  119. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  120. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  121. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  122. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  123. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  124. package/src/resources/extensions/gsd/auto-prompts.ts +20 -1
  125. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  126. package/src/resources/extensions/gsd/auto.ts +123 -10
  127. package/src/resources/extensions/gsd/commands.ts +245 -22
  128. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  129. package/src/resources/extensions/gsd/docs/preferences-reference.md +201 -2
  130. package/src/resources/extensions/gsd/files.ts +123 -1
  131. package/src/resources/extensions/gsd/guided-flow.ts +237 -4
  132. package/src/resources/extensions/gsd/index.ts +47 -3
  133. package/src/resources/extensions/gsd/paths.ts +9 -0
  134. package/src/resources/extensions/gsd/preferences.ts +59 -1
  135. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  136. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  137. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  138. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  139. package/src/resources/extensions/gsd/state.ts +15 -3
  140. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  141. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  142. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  143. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  144. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  145. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  146. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  147. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  148. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  149. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  150. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  151. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  152. package/src/resources/extensions/gsd/worktree.ts +22 -0
  153. package/src/resources/extensions/shared/next-action-ui.ts +16 -1
@@ -13,6 +13,7 @@ import { deriveState } from "./state.js";
13
13
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
14
14
  import { showQueue, showDiscuss } from "./guided-flow.js";
15
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,24 +175,24 @@ 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
194
  // Not running in this process — check for a remote auto-mode session
182
- const result = stopAutoRemote(process.cwd());
195
+ const result = stopAutoRemote(projectRoot());
183
196
  if (result.found) {
184
197
  ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
185
198
  } else if (result.error) {
@@ -207,42 +220,42 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
207
220
  }
208
221
 
209
222
  if (trimmed === "history" || trimmed.startsWith("history ")) {
210
- await handleHistory(trimmed.replace(/^history\s*/, "").trim(), ctx, process.cwd());
223
+ await handleHistory(trimmed.replace(/^history\s*/, "").trim(), ctx, projectRoot());
211
224
  return;
212
225
  }
213
226
 
214
227
  if (trimmed === "undo" || trimmed.startsWith("undo ")) {
215
- await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi, process.cwd());
228
+ await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi, projectRoot());
216
229
  return;
217
230
  }
218
231
 
219
232
  if (trimmed.startsWith("skip ")) {
220
- await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, process.cwd());
233
+ await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, projectRoot());
221
234
  return;
222
235
  }
223
236
 
224
237
  if (trimmed === "export" || trimmed.startsWith("export ")) {
225
- await handleExport(trimmed.replace(/^export\s*/, "").trim(), ctx, process.cwd());
238
+ await handleExport(trimmed.replace(/^export\s*/, "").trim(), ctx, projectRoot());
226
239
  return;
227
240
  }
228
241
 
229
242
  if (trimmed === "cleanup branches") {
230
- await handleCleanupBranches(ctx, process.cwd());
243
+ await handleCleanupBranches(ctx, projectRoot());
231
244
  return;
232
245
  }
233
246
 
234
247
  if (trimmed === "cleanup snapshots") {
235
- await handleCleanupSnapshots(ctx, process.cwd());
248
+ await handleCleanupSnapshots(ctx, projectRoot());
236
249
  return;
237
250
  }
238
251
 
239
252
  if (trimmed === "queue") {
240
- await showQueue(ctx, pi, process.cwd());
253
+ await showQueue(ctx, pi, projectRoot());
241
254
  return;
242
255
  }
243
256
 
244
257
  if (trimmed === "discuss") {
245
- await showDiscuss(ctx, pi, process.cwd());
258
+ await showDiscuss(ctx, pi, projectRoot());
246
259
  return;
247
260
  }
248
261
 
@@ -266,6 +279,15 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
266
279
  return;
267
280
  }
268
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
+
269
291
  if (trimmed === "migrate" || trimmed.startsWith("migrate ")) {
270
292
  const { handleMigrate } = await import("./migrate/command.js");
271
293
  await handleMigrate(trimmed.replace(/^migrate\s*/, "").trim(), ctx, pi);
@@ -279,12 +301,12 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
279
301
 
280
302
  if (trimmed === "") {
281
303
  // Bare /gsd defaults to step mode
282
- await startAuto(ctx, pi, process.cwd(), false, { step: true });
304
+ await startAuto(ctx, pi, projectRoot(), false, { step: true });
283
305
  return;
284
306
  }
285
307
 
286
308
  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>.`,
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>.`,
288
310
  "warning",
289
311
  );
290
312
  },
@@ -292,7 +314,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
292
314
  }
293
315
 
294
316
  async function handleStatus(ctx: ExtensionCommandContext): Promise<void> {
295
- const basePath = process.cwd();
317
+ const basePath = projectRoot();
296
318
  const state = await deriveState(basePath);
297
319
 
298
320
  if (state.registry.length === 0) {
@@ -376,9 +398,9 @@ async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: Exte
376
398
  const parts = trimmed ? trimmed.split(/\s+/) : [];
377
399
  const mode = parts[0] === "fix" || parts[0] === "heal" || parts[0] === "audit" ? parts[0] : "doctor";
378
400
  const requestedScope = mode === "doctor" ? parts[0] : parts[1];
379
- const scope = await selectDoctorScope(process.cwd(), requestedScope);
401
+ const scope = await selectDoctorScope(projectRoot(), requestedScope);
380
402
  const effectiveScope = mode === "audit" ? requestedScope : scope;
381
- const report = await runGSDDoctor(process.cwd(), {
403
+ const report = await runGSDDoctor(projectRoot(), {
382
404
  fix: mode === "fix" || mode === "heal",
383
405
  scope: effectiveScope,
384
406
  });
@@ -495,8 +517,10 @@ async function handlePrefsWizard(
495
517
  prefs.auto_supervisor = autoSup;
496
518
  }
497
519
 
498
- // ─── Git main branch ────────────────────────────────────────────────────
520
+ // ─── Git settings ───────────────────────────────────────────────────────
499
521
  const git: Record<string, unknown> = (prefs.git as Record<string, unknown>) ?? {};
522
+
523
+ // main_branch
500
524
  const currentBranch = git.main_branch ? String(git.main_branch) : "";
501
525
  const branchInput = await ctx.ui.input(
502
526
  `Git main branch${currentBranch ? ` (current: ${currentBranch})` : ""}:`,
@@ -510,6 +534,90 @@ async function handlePrefsWizard(
510
534
  delete git.main_branch;
511
535
  }
512
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
+
513
621
  // ─── Git commit_docs ────────────────────────────────────────────────────
514
622
  const currentCommitDocs = git.commit_docs;
515
623
  const commitDocsChoice = await ctx.ui.select(
@@ -544,6 +652,89 @@ async function handlePrefsWizard(
544
652
  prefs.unique_milestone_ids = uniqueChoice === "true";
545
653
  }
546
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
+
547
738
  // ─── Serialize to frontmatter ───────────────────────────────────────────
548
739
  prefs.version = prefs.version || 1;
549
740
  const frontmatter = serializePreferencesToFrontmatter(prefs);
@@ -634,7 +825,10 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
634
825
  const orderedKeys = [
635
826
  "version", "always_use_skills", "prefer_skills", "avoid_skills",
636
827
  "skill_rules", "custom_instructions", "models", "skill_discovery",
637
- "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",
638
832
  ];
639
833
 
640
834
  const seen = new Set<string>();
@@ -972,6 +1166,35 @@ async function handleCleanupSnapshots(ctx: ExtensionCommandContext, basePath: st
972
1166
  ctx.ui.notify(`Pruned ${pruned} old snapshot refs. ${refs.length - pruned} remain.`, "success");
973
1167
  }
974
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
+
975
1198
  async function handleSteer(change: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
976
1199
  const basePath = process.cwd();
977
1200
  const state = await deriveState(basePath);
@@ -5,7 +5,7 @@ import { readFileSync } from "node:fs";
5
5
  import { readdirSync } from "node:fs";
6
6
  import { resolveMilestoneFile, milestonesDir } from "./paths.js";
7
7
  import { parseRoadmapSlices } from "./roadmap-slices.js";
8
- import { extractMilestoneSeq, milestoneIdSort } from "./guided-flow.js";
8
+ import { findMilestoneIds } from "./guided-flow.js";
9
9
 
10
10
  const SLICE_DISPATCH_TYPES = new Set([
11
11
  "research-slice",
@@ -43,24 +43,12 @@ export function getPriorSliceCompletionBlocker(base: string, _mainBranch: string
43
43
  const [targetMid, targetSid] = unitId.split("/");
44
44
  if (!targetMid || !targetSid) return null;
45
45
 
46
- const targetSeq = extractMilestoneSeq(targetMid);
47
- if (targetSeq === 0) return null;
48
-
49
- // Scan actual milestone directories instead of iterating by number
50
- let milestoneIds: string[];
51
- try {
52
- milestoneIds = readdirSync(milestonesDir(base), { withFileTypes: true })
53
- .filter(d => d.isDirectory())
54
- .map(d => {
55
- const match = d.name.match(/^(M\d+(?:-[a-z0-9]{6})?)/);
56
- return match ? match[1] : null;
57
- })
58
- .filter((id): id is string => id !== null)
59
- .sort(milestoneIdSort)
60
- .filter(id => extractMilestoneSeq(id) <= targetSeq);
61
- } catch {
62
- return null;
63
- }
46
+ // Use findMilestoneIds to respect custom queue order.
47
+ // Only check milestones that come BEFORE the target in queue order.
48
+ const allIds = findMilestoneIds(base);
49
+ const targetIdx = allIds.indexOf(targetMid);
50
+ if (targetIdx < 0) return null;
51
+ const milestoneIds = allIds.slice(0, targetIdx + 1);
64
52
 
65
53
  for (const mid of milestoneIds) {
66
54
  // Read from disk (working tree) — always has the latest state