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
@@ -28,10 +28,11 @@ import { createBashTool, createWriteTool, createReadTool, createEditTool, isTool
28
28
  import { registerGSDCommand, loadToolApiKeys } from "./commands.js";
29
29
  import { registerExitCommand } from "./exit-command.js";
30
30
  import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
31
+ import { getActiveAutoWorktreeContext } from "./auto-worktree.js";
31
32
  import { saveFile, formatContinue, loadFile, parseContinue, parseSummary, loadActiveOverrides, formatOverridesSection } from "./files.js";
32
33
  import { loadPrompt } from "./prompt-loader.js";
33
34
  import { deriveState } from "./state.js";
34
- import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData } from "./auto.js";
35
+ import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData, markToolStart, markToolEnd } from "./auto.js";
35
36
  import { saveActivityLog } from "./activity-log.js";
36
37
  import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId } from "./guided-flow.js";
37
38
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
@@ -47,10 +48,11 @@ import {
47
48
  resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTaskFiles, resolveTasksDir,
48
49
  relSliceFile, relSlicePath, relTaskFile,
49
50
  buildSliceFileName, buildMilestoneFileName, gsdRoot, resolveMilestonePath,
51
+ resolveGsdRootFile,
50
52
  } from "./paths.js";
51
53
  import { Key } from "@gsd/pi-tui";
52
54
  import { join } from "node:path";
53
- import { existsSync } from "node:fs";
55
+ import { existsSync, readFileSync } from "node:fs";
54
56
  import { shortcutDesc } from "../shared/terminal.js";
55
57
  import { Text } from "@gsd/pi-tui";
56
58
  import { pauseAutoForProviderError } from "./provider-error-pause.js";
@@ -272,6 +274,20 @@ export default function (pi: ExtensionAPI) {
272
274
  }
273
275
  }
274
276
 
277
+ // Load project knowledge if available
278
+ let knowledgeBlock = "";
279
+ const knowledgePath = resolveGsdRootFile(process.cwd(), "KNOWLEDGE");
280
+ if (existsSync(knowledgePath)) {
281
+ try {
282
+ const content = readFileSync(knowledgePath, "utf-8").trim();
283
+ if (content) {
284
+ knowledgeBlock = `\n\n[PROJECT KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${content}`;
285
+ }
286
+ } catch {
287
+ // File read error — skip knowledge injection
288
+ }
289
+ }
290
+
275
291
  // Detect skills installed during this auto-mode session
276
292
  let newSkillsBlock = "";
277
293
  if (hasSkillSnapshot()) {
@@ -287,6 +303,7 @@ export default function (pi: ExtensionAPI) {
287
303
  let worktreeBlock = "";
288
304
  const worktreeName = getActiveWorktreeName();
289
305
  const worktreeMainCwd = getWorktreeOriginalCwd();
306
+ const autoWorktree = getActiveAutoWorktreeContext();
290
307
  if (worktreeName && worktreeMainCwd) {
291
308
  worktreeBlock = [
292
309
  "",
@@ -304,10 +321,27 @@ export default function (pi: ExtensionAPI) {
304
321
  "All file operations, bash commands, and GSD state resolve against the worktree path above.",
305
322
  "Use /worktree merge to merge changes back. Use /worktree return to switch back to the main tree.",
306
323
  ].join("\n");
324
+ } else if (autoWorktree) {
325
+ worktreeBlock = [
326
+ "",
327
+ "",
328
+ "[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
329
+ `IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
330
+ `The actual current working directory is: ${process.cwd()}`,
331
+ "",
332
+ "You are working inside a GSD auto-worktree.",
333
+ `- Milestone worktree: ${autoWorktree.worktreeName}`,
334
+ `- Worktree path (this is the real cwd): ${process.cwd()}`,
335
+ `- Main project: ${autoWorktree.originalBase}`,
336
+ `- Branch: ${autoWorktree.branch}`,
337
+ "",
338
+ "All file operations, bash commands, and GSD state resolve against the worktree path above.",
339
+ "Write every .gsd artifact in the worktree path above, never in the main project tree.",
340
+ ].join("\n");
307
341
  }
308
342
 
309
343
  return {
310
- systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${newSkillsBlock}${worktreeBlock}`,
344
+ systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${newSkillsBlock}${worktreeBlock}`,
311
345
  ...(injection
312
346
  ? {
313
347
  message: {
@@ -542,6 +576,16 @@ export default function (pi: ExtensionAPI) {
542
576
  const existing = await loadFile(discussionPath) ?? `# ${milestoneId} Discussion Log\n\n`;
543
577
  await saveFile(discussionPath, existing + newBlock);
544
578
  });
579
+
580
+ // ── tool_execution_start/end: track in-flight tools for idle detection ──
581
+ pi.on("tool_execution_start", async (event) => {
582
+ if (!isAutoActive()) return;
583
+ markToolStart(event.toolCallId);
584
+ });
585
+
586
+ pi.on("tool_execution_end", async (event) => {
587
+ markToolEnd(event.toolCallId);
588
+ });
545
589
  }
546
590
 
547
591
  async function buildGuidedExecuteContextInjection(prompt: string, basePath: string): Promise<string | null> {
@@ -15,6 +15,9 @@ import { nativeScanGsdTree, type GsdTreeEntry } from "./native-parser-bridge.js"
15
15
 
16
16
  // ─── Directory Listing Cache ──────────────────────────────────────────────────
17
17
 
18
+ /** Max entries before eviction. Prevents unbounded growth in long sessions (#611). */
19
+ const DIR_CACHE_MAX = 200;
20
+
18
21
  const dirEntryCache = new Map<string, Dirent[]>();
19
22
  const dirListCache = new Map<string, string[]>();
20
23
 
@@ -85,6 +88,7 @@ function cachedReaddirWithTypes(dirPath: string): Dirent[] {
85
88
  d.isSocket = () => false;
86
89
  return d;
87
90
  });
91
+ if (dirEntryCache.size >= DIR_CACHE_MAX) dirEntryCache.clear();
88
92
  dirEntryCache.set(dirPath, dirents);
89
93
  return dirents;
90
94
  }
@@ -92,6 +96,7 @@ function cachedReaddirWithTypes(dirPath: string): Dirent[] {
92
96
  }
93
97
 
94
98
  const entries = readdirSync(dirPath, { withFileTypes: true });
99
+ if (dirEntryCache.size >= DIR_CACHE_MAX) dirEntryCache.clear();
95
100
  dirEntryCache.set(dirPath, entries);
96
101
  return entries;
97
102
  }
@@ -107,6 +112,7 @@ function cachedReaddir(dirPath: string): string[] {
107
112
  const treeEntries = nativeTreeCache.get(key);
108
113
  if (treeEntries) {
109
114
  const names = treeEntries.map(e => e.name);
115
+ if (dirListCache.size >= DIR_CACHE_MAX) dirListCache.clear();
110
116
  dirListCache.set(dirPath, names);
111
117
  return names;
112
118
  }
@@ -114,6 +120,7 @@ function cachedReaddir(dirPath: string): string[] {
114
120
  }
115
121
 
116
122
  const entries = readdirSync(dirPath);
123
+ if (dirListCache.size >= DIR_CACHE_MAX) dirListCache.clear();
117
124
  dirListCache.set(dirPath, entries);
118
125
  return entries;
119
126
  }
@@ -248,6 +255,7 @@ export const GSD_ROOT_FILES = {
248
255
  STATE: "STATE.md",
249
256
  REQUIREMENTS: "REQUIREMENTS.md",
250
257
  OVERRIDES: "OVERRIDES.md",
258
+ KNOWLEDGE: "KNOWLEDGE.md",
251
259
  } as const;
252
260
 
253
261
  export type GSDRootFileKey = keyof typeof GSD_ROOT_FILES;
@@ -259,6 +267,7 @@ const LEGACY_GSD_ROOT_FILES: Record<GSDRootFileKey, string> = {
259
267
  STATE: "state.md",
260
268
  REQUIREMENTS: "requirements.md",
261
269
  OVERRIDES: "overrides.md",
270
+ KNOWLEDGE: "knowledge.md",
262
271
  };
263
272
 
264
273
  export function gsdRoot(basePath: string): string {
@@ -1,4 +1,4 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { isAbsolute, join } from "node:path";
4
4
  import { getAgentDir } from "@gsd/pi-coding-agent";
@@ -1252,3 +1252,61 @@ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] {
1252
1252
  return (prefs?.preferences.pre_dispatch_hooks ?? [])
1253
1253
  .filter(h => h.enabled !== false);
1254
1254
  }
1255
+
1256
+ /**
1257
+ * Validate a model ID string.
1258
+ * Returns true if the ID looks like a valid model identifier.
1259
+ */
1260
+ export function validateModelId(modelId: string): boolean {
1261
+ if (!modelId || typeof modelId !== "string") return false;
1262
+ const trimmed = modelId.trim();
1263
+ if (trimmed.length === 0 || trimmed.length > 256) return false;
1264
+ // Allow alphanumeric, hyphens, underscores, dots, slashes, colons
1265
+ return /^[a-zA-Z0-9\-_./:]+$/.test(trimmed);
1266
+ }
1267
+
1268
+ /**
1269
+ * Update the models section of the global GSD preferences file.
1270
+ * Performs a safe read-modify-write: reads current content, updates the models
1271
+ * YAML block, and writes back. Creates the file if it doesn't exist.
1272
+ */
1273
+ export function updatePreferencesModels(models: GSDModelConfigV2): void {
1274
+ const prefsPath = getGlobalGSDPreferencesPath();
1275
+
1276
+ let content = "";
1277
+ if (existsSync(prefsPath)) {
1278
+ content = readFileSync(prefsPath, "utf-8");
1279
+ }
1280
+
1281
+ // Build the new models block
1282
+ const lines: string[] = ["models:"];
1283
+ for (const [phase, value] of Object.entries(models)) {
1284
+ if (typeof value === "string") {
1285
+ lines.push(` ${phase}: ${value}`);
1286
+ } else if (value && typeof value === "object") {
1287
+ const config = value as GSDPhaseModelConfig;
1288
+ lines.push(` ${phase}:`);
1289
+ lines.push(` model: ${config.model}`);
1290
+ if (config.provider) {
1291
+ lines.push(` provider: ${config.provider}`);
1292
+ }
1293
+ if (config.fallbacks && config.fallbacks.length > 0) {
1294
+ lines.push(` fallbacks:`);
1295
+ for (const fb of config.fallbacks) {
1296
+ lines.push(` - ${fb}`);
1297
+ }
1298
+ }
1299
+ }
1300
+ }
1301
+ const modelsBlock = lines.join("\n");
1302
+
1303
+ // Replace existing models block or append
1304
+ const modelsRegex = /^models:[\s\S]*?(?=\n[a-z_]|\n*$)/m;
1305
+ if (modelsRegex.test(content)) {
1306
+ content = content.replace(modelsRegex, modelsBlock);
1307
+ } else {
1308
+ content = content.trimEnd() + "\n\n" + modelsBlock + "\n";
1309
+ }
1310
+
1311
+ writeFileSync(prefsPath, content, "utf-8");
1312
+ }
@@ -54,11 +54,12 @@ Then:
54
54
  - Don't fix symptoms. Understand *why* something fails before changing code. A test that passes after a change you don't understand is luck, not a fix.
55
55
  11. **Blocker discovery:** If execution reveals that the remaining slice plan is fundamentally invalid — not just a bug or minor deviation, but a plan-invalidating finding like a wrong API, missing capability, or architectural mismatch — set `blocker_discovered: true` in the task summary frontmatter and describe the blocker clearly in the summary narrative. Do NOT set `blocker_discovered: true` for ordinary debugging, minor deviations, or issues that can be fixed within the current task or the remaining plan. This flag triggers an automatic replan of the slice.
56
56
  12. If you made an architectural, pattern, library, or observability decision during this task that downstream work should know about, append it to `.gsd/DECISIONS.md` (use the **Decisions** output template from the inlined templates below if the file doesn't exist yet). Not every task produces decisions — only append when a meaningful choice was made.
57
- 13. Use the **Task Summary** output template from the inlined templates below
58
- 14. Write `{{taskSummaryPath}}`
59
- 15. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
60
- 16. Do not commit manually the system auto-commits your changes after this unit completes.
61
- 17. Update `.gsd/STATE.md`
57
+ 13. If you discover a non-obvious rule, recurring gotcha, or useful pattern during execution, append it to `.gsd/KNOWLEDGE.md`. Only add entries that would save future agents from repeating your investigation. Don't add obvious things.
58
+ 14. Use the **Task Summary** output template from the inlined templates below
59
+ 15. Write `{{taskSummaryPath}}`
60
+ 16. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
61
+ 17. Do not commit manually — the system auto-commits your changes after this unit completes.
62
+ 18. Update `.gsd/STATE.md`
62
63
 
63
64
  All work stays in your working directory: `{{workingDirectory}}`.
64
65
 
@@ -65,6 +65,7 @@ Titles live inside file content (headings, frontmatter), not in file or director
65
65
  PROJECT.md (living doc - what the project is right now)
66
66
  REQUIREMENTS.md (requirement contract - tracks active/validated/deferred/out-of-scope)
67
67
  DECISIONS.md (append-only register of architectural and pattern decisions)
68
+ KNOWLEDGE.md (append-only register of project-specific rules, patterns, and lessons learned)
68
69
  OVERRIDES.md (user-issued overrides that supersede plan content via /gsd steer)
69
70
  QUEUE.md (append-only log of queued milestones via /gsd queue)
70
71
  STATE.md
@@ -100,6 +101,7 @@ All auto-mode work happens inside a worktree at `.gsd/worktrees/<MID>/`. This is
100
101
  - **PROJECT.md** is a living document describing what the project is right now - current state only, updated at slice completion when stale
101
102
  - **REQUIREMENTS.md** tracks the requirement contract — requirements move between Active, Validated, Deferred, Blocked, and Out of Scope as slices prove or invalidate them. Update at slice completion when evidence supports a status change.
102
103
  - **DECISIONS.md** is an append-only register of architectural and pattern decisions - read it during planning/research, append to it during execution when a meaningful decision is made
104
+ - **KNOWLEDGE.md** is an append-only register of project-specific rules, patterns, and lessons learned. Read it at the start of every unit. Append to it when you discover a recurring issue, a non-obvious pattern, or a rule that future agents should follow.
103
105
  - **CONTEXT.md** files (milestone or slice level) capture the brief — scope, goals, constraints, and key decisions from discussion. When present, they are the authoritative source for what a milestone or slice is trying to achieve. Read them before planning or executing.
104
106
  - **Milestones** are major project phases (M001, M002, ...)
105
107
  - **Slices** are demoable vertical increments (S01, S02, ...) ordered by risk. After each slice completes, the roadmap is reassessed before the next slice begins.
@@ -0,0 +1,231 @@
1
+ /**
2
+ * GSD Queue Order — Custom milestone execution ordering.
3
+ *
4
+ * Stores an explicit execution order in `.gsd/QUEUE-ORDER.json`.
5
+ * When present, `findMilestoneIds()` uses this order instead of
6
+ * the default numeric sort (milestoneIdSort).
7
+ *
8
+ * The file is committed to git (not gitignored) so ordering
9
+ * survives branch switches and is shared across sessions.
10
+ */
11
+
12
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { gsdRoot } from "./paths.js";
15
+ import { milestoneIdSort } from "./guided-flow.js";
16
+
17
+ // ─── Types ───────────────────────────────────────────────────────────────────
18
+
19
+ interface QueueOrderFile {
20
+ order: string[];
21
+ updatedAt: string;
22
+ }
23
+
24
+ export interface DependencyViolation {
25
+ milestone: string;
26
+ dependsOn: string;
27
+ type: 'would_block' | 'circular' | 'missing_dep';
28
+ message: string;
29
+ }
30
+
31
+ export interface DependencyRedundancy {
32
+ milestone: string;
33
+ dependsOn: string;
34
+ }
35
+
36
+ export interface DependencyValidation {
37
+ valid: boolean;
38
+ violations: DependencyViolation[];
39
+ redundant: DependencyRedundancy[];
40
+ }
41
+
42
+ // ─── Path ────────────────────────────────────────────────────────────────────
43
+
44
+ function queueOrderPath(basePath: string): string {
45
+ return join(gsdRoot(basePath), "QUEUE-ORDER.json");
46
+ }
47
+
48
+ // ─── Read / Write ────────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Load the custom queue order. Returns null if no file exists or if
52
+ * the file is corrupt/unreadable.
53
+ */
54
+ export function loadQueueOrder(basePath: string): string[] | null {
55
+ const p = queueOrderPath(basePath);
56
+ if (!existsSync(p)) return null;
57
+ try {
58
+ const data: QueueOrderFile = JSON.parse(readFileSync(p, "utf-8"));
59
+ if (!Array.isArray(data.order)) return null;
60
+ return data.order;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Save a custom queue order to disk.
68
+ */
69
+ export function saveQueueOrder(basePath: string, order: string[]): void {
70
+ const data: QueueOrderFile = {
71
+ order,
72
+ updatedAt: new Date().toISOString(),
73
+ };
74
+ writeFileSync(queueOrderPath(basePath), JSON.stringify(data, null, 2) + "\n", "utf-8");
75
+ }
76
+
77
+ // ─── Sorting ─────────────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Sort milestone IDs respecting a custom order.
81
+ *
82
+ * - IDs present in `customOrder` appear in that exact sequence.
83
+ * - IDs on disk but NOT in `customOrder` are appended at the end,
84
+ * sorted by the default `milestoneIdSort` (numeric).
85
+ * - IDs in `customOrder` but NOT on disk are silently skipped.
86
+ * - When `customOrder` is null, falls back to `milestoneIdSort`.
87
+ */
88
+ export function sortByQueueOrder(ids: string[], customOrder: string[] | null): string[] {
89
+ if (!customOrder) return [...ids].sort(milestoneIdSort);
90
+
91
+ const idSet = new Set(ids);
92
+ const ordered: string[] = [];
93
+
94
+ // First: IDs from customOrder that exist on disk
95
+ for (const id of customOrder) {
96
+ if (idSet.has(id)) {
97
+ ordered.push(id);
98
+ idSet.delete(id);
99
+ }
100
+ }
101
+
102
+ // Then: remaining IDs not in customOrder, in default sort order
103
+ const remaining = [...idSet].sort(milestoneIdSort);
104
+ return [...ordered, ...remaining];
105
+ }
106
+
107
+ // ─── Pruning ─────────────────────────────────────────────────────────────────
108
+
109
+ /**
110
+ * Remove IDs from the queue order file that are no longer valid
111
+ * (completed or deleted milestones). No-op if file doesn't exist.
112
+ */
113
+ export function pruneQueueOrder(basePath: string, validIds: string[]): void {
114
+ const order = loadQueueOrder(basePath);
115
+ if (!order) return;
116
+
117
+ const validSet = new Set(validIds);
118
+ const pruned = order.filter(id => validSet.has(id));
119
+
120
+ if (pruned.length !== order.length) {
121
+ saveQueueOrder(basePath, pruned);
122
+ }
123
+ }
124
+
125
+ // ─── Validation ──────────────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Validate a proposed queue order against dependency constraints.
129
+ *
130
+ * Checks:
131
+ * - would_block: A milestone is placed before one of its dependencies
132
+ * - circular: Two or more milestones form a dependency cycle
133
+ * - missing_dep: A milestone depends on an ID that doesn't exist
134
+ * - redundant: A dependency is satisfied by queue position (dep comes earlier)
135
+ */
136
+ export function validateQueueOrder(
137
+ order: string[],
138
+ depsMap: Map<string, string[]>,
139
+ completedIds: Set<string>,
140
+ ): DependencyValidation {
141
+ const violations: DependencyViolation[] = [];
142
+ const redundant: DependencyRedundancy[] = [];
143
+
144
+ const positionMap = new Map<string, number>();
145
+ for (let i = 0; i < order.length; i++) {
146
+ positionMap.set(order[i], i);
147
+ }
148
+
149
+ const allKnownIds = new Set([...order, ...completedIds]);
150
+
151
+ for (const [mid, deps] of depsMap) {
152
+ const midPos = positionMap.get(mid);
153
+ if (midPos === undefined) continue; // not in pending order
154
+
155
+ for (const dep of deps) {
156
+ // Dep already completed — always satisfied
157
+ if (completedIds.has(dep)) continue;
158
+
159
+ // Dep doesn't exist anywhere
160
+ if (!allKnownIds.has(dep)) {
161
+ violations.push({
162
+ milestone: mid,
163
+ dependsOn: dep,
164
+ type: 'missing_dep',
165
+ message: `${mid} depends on ${dep}, but ${dep} does not exist.`,
166
+ });
167
+ continue;
168
+ }
169
+
170
+ const depPos = positionMap.get(dep);
171
+ if (depPos === undefined) continue; // dep not in pending order (edge case)
172
+
173
+ if (depPos > midPos) {
174
+ // Dep comes AFTER this milestone in the order — violation
175
+ violations.push({
176
+ milestone: mid,
177
+ dependsOn: dep,
178
+ type: 'would_block',
179
+ message: `${mid} cannot run before ${dep} — ${mid} depends_on: [${dep}].`,
180
+ });
181
+ } else {
182
+ // Dep comes before — satisfied by position, redundant
183
+ redundant.push({ milestone: mid, dependsOn: dep });
184
+ }
185
+ }
186
+ }
187
+
188
+ // Check for circular dependencies
189
+ const visited = new Set<string>();
190
+ const inStack = new Set<string>();
191
+
192
+ function hasCycle(node: string, path: string[]): string[] | null {
193
+ if (inStack.has(node)) return [...path, node];
194
+ if (visited.has(node)) return null;
195
+
196
+ visited.add(node);
197
+ inStack.add(node);
198
+
199
+ const deps = depsMap.get(node) ?? [];
200
+ for (const dep of deps) {
201
+ if (completedIds.has(dep)) continue;
202
+ const cycle = hasCycle(dep, [...path, node]);
203
+ if (cycle) return cycle;
204
+ }
205
+
206
+ inStack.delete(node);
207
+ return null;
208
+ }
209
+
210
+ for (const mid of order) {
211
+ if (!visited.has(mid)) {
212
+ const cycle = hasCycle(mid, []);
213
+ if (cycle) {
214
+ const cycleStr = cycle.join(' → ');
215
+ violations.push({
216
+ milestone: cycle[0],
217
+ dependsOn: cycle[cycle.length - 2],
218
+ type: 'circular',
219
+ message: `Circular dependency: ${cycleStr}`,
220
+ });
221
+ break; // one cycle report is enough
222
+ }
223
+ }
224
+ }
225
+
226
+ return {
227
+ valid: violations.length === 0,
228
+ violations,
229
+ redundant,
230
+ };
231
+ }