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
@@ -80,9 +80,9 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
80
80
 
81
81
  - `skill_rules`: situational rules with a human-readable `when` trigger and one or more of `use`, `prefer`, or `avoid`.
82
82
 
83
- - `custom_instructions`: extra durable instructions related to skill use.
83
+ - `custom_instructions`: extra durable instructions related to skill use. For operational project knowledge (recurring rules, gotchas, patterns), use `.gsd/KNOWLEDGE.md` instead — it's injected into every agent prompt automatically and agents can append to it during execution.
84
84
 
85
- - `models`: per-stage model selection for auto-mode. Keys: `research`, `planning`, `execution`, `completion`. Values can be:
85
+ - `models`: per-stage model selection for auto-mode. Keys: `research`, `planning`, `execution`, `execution_simple`, `completion`, `subagent`. Values can be:
86
86
  - Simple string: `"claude-sonnet-4-6"` — single model, no fallbacks
87
87
  - Provider-qualified string: `"bedrock/claude-sonnet-4-6"` — targets a specific provider when the same model ID exists across multiple providers
88
88
  - Object with fallbacks: `{ model: "claude-opus-4-6", fallbacks: ["glm-5", "minimax-m2.5"] }` — tries fallbacks in order if primary fails
@@ -108,10 +108,75 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
108
108
  - `pre_merge_check`: boolean or `"auto"` — run pre-merge checks before merging a worktree back to the integration branch. `true` always runs, `false` never runs, `"auto"` runs when CI is detected. Default: `false`.
109
109
  - `commit_type`: string — override the conventional commit type prefix. Must be one of: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `build`, `style`. Default: inferred from diff content.
110
110
  - `main_branch`: string — the primary branch name for new git repos (e.g., `"main"`, `"master"`, `"trunk"`). Also used by `getMainBranch()` as the preferred branch when auto-detection is ambiguous. Default: `"main"`.
111
+ - `merge_strategy`: `"squash"` or `"merge"` — controls how worktree branches are merged back. `"squash"` combines all commits into one; `"merge"` preserves individual commits. Default: `"squash"`.
112
+ - `isolation`: `"worktree"` or `"branch"` — controls auto-mode git isolation strategy. `"worktree"` creates a milestone worktree for isolated work; `"branch"` works directly in the project root (useful for submodule-heavy repos). Default: `"worktree"`.
111
113
  - `commit_docs`: boolean — when `false`, prevents GSD from committing `.gsd/` planning artifacts to git. The `.gsd/` folder is added to `.gitignore` and kept local-only. Useful for teams where only some members use GSD, or when company policy requires a clean repository. Default: `true`.
112
114
 
113
115
  - `unique_milestone_ids`: boolean — when `true`, generates milestone IDs in `M{seq}-{rand6}` format (e.g. `M001-eh88as`) instead of plain sequential `M001`. Prevents ID collisions in team workflows where multiple contributors create milestones concurrently. Both formats coexist — existing `M001`-style milestones remain valid. Default: `false`.
114
116
 
117
+ - `budget_ceiling`: number — maximum dollar amount to spend on auto-mode. When reached, behavior is controlled by `budget_enforcement`. Default: no limit.
118
+
119
+ - `budget_enforcement`: `"warn"`, `"pause"`, or `"halt"` — action taken when `budget_ceiling` is reached.
120
+ - `warn` — log a warning but continue execution.
121
+ - `pause` — pause auto-mode and wait for user confirmation.
122
+ - `halt` — stop auto-mode immediately.
123
+ - Default: `"pause"`.
124
+
125
+ - `context_pause_threshold`: number (0-100) — context window usage percentage at which auto-mode should pause to suggest checkpointing. Set to `0` to disable. Default: `0` (disabled).
126
+
127
+ - `token_profile`: `"budget"`, `"balanced"`, or `"quality"` — coordinates model selection, phase skipping, and context compression. `budget` skips research/reassessment and uses cheaper models; `balanced` (default) runs all phases; `quality` prefers higher-quality models. See token-optimization docs.
128
+
129
+ - `phases`: fine-grained control over which phases run. Usually set by `token_profile`, but can be overridden. Keys:
130
+ - `skip_research`: boolean — skip milestone-level research. Default: `false`.
131
+ - `skip_reassess`: boolean — skip roadmap reassessment after each slice. Default: `false`.
132
+ - `skip_slice_research`: boolean — skip per-slice research. Default: `false`.
133
+
134
+ - `remote_questions`: route interactive questions to Slack/Discord for headless auto-mode. Keys:
135
+ - `channel`: `"slack"` or `"discord"` — channel type.
136
+ - `channel_id`: string or number — channel ID.
137
+ - `timeout_minutes`: number — question timeout in minutes (clamped 1-30).
138
+ - `poll_interval_seconds`: number — poll interval in seconds (clamped 2-30).
139
+
140
+ - `notifications`: configures desktop notification behavior during auto-mode. Keys:
141
+ - `enabled`: boolean — master toggle for all notifications. Default: `true`.
142
+ - `on_complete`: boolean — notify when a unit completes. Default: `true`.
143
+ - `on_error`: boolean — notify on errors. Default: `true`.
144
+ - `on_budget`: boolean — notify when budget thresholds are reached. Default: `true`.
145
+ - `on_milestone`: boolean — notify when a milestone finishes. Default: `true`.
146
+ - `on_attention`: boolean — notify when manual attention is needed. Default: `true`.
147
+
148
+ - `uat_dispatch`: boolean — when `true`, enables UAT (User Acceptance Testing) dispatch mode. Default: `false`.
149
+
150
+ - `post_unit_hooks`: array — hooks that fire after a unit completes. Each entry has:
151
+ - `name`: string — unique hook identifier.
152
+ - `after`: string[] — unit types that trigger this hook (e.g., `["execute-task"]`).
153
+ - `prompt`: string — prompt sent to the LLM. Supports `{milestoneId}`, `{sliceId}`, `{taskId}` substitutions.
154
+ - `max_cycles`: number — max times this hook fires per trigger (default: 1, max: 10).
155
+ - `model`: string — optional model override.
156
+ - `artifact`: string — expected output file name (relative to task/slice dir). Hook is skipped if file already exists (idempotent).
157
+ - `retry_on`: string — if this file is produced instead of the artifact, re-run the trigger unit then re-run hooks.
158
+ - `agent`: string — agent definition file to use for hook execution.
159
+ - `enabled`: boolean — toggle without removing (default: `true`).
160
+
161
+ - `pre_dispatch_hooks`: array — hooks that fire before a unit is dispatched. Each entry has:
162
+ - `name`: string — unique hook identifier.
163
+ - `before`: string[] — unit types to intercept.
164
+ - `action`: `"modify"`, `"skip"`, or `"replace"` — what to do with the unit.
165
+ - `prepend`: string — text prepended to unit prompt (for `"modify"` action).
166
+ - `append`: string — text appended to unit prompt (for `"modify"` action).
167
+ - `prompt`: string — replacement prompt (for `"replace"` action; required when action is `"replace"`).
168
+ - `unit_type`: string — override unit type label (for `"replace"` action).
169
+ - `skip_if`: string — for `"skip"` action: only skip if this file exists (relative to unit dir).
170
+ - `model`: string — optional model override when this hook fires.
171
+ - `enabled`: boolean — toggle without removing (default: `true`).
172
+
173
+ **Action validation:**
174
+ - `"modify"` requires at least one of `prepend` or `append`.
175
+ - `"replace"` requires `prompt`.
176
+ - `"skip"` is valid with no additional fields.
177
+
178
+ **Known unit types for `before`/`after`:** `research-milestone`, `plan-milestone`, `research-slice`, `plan-slice`, `execute-task`, `complete-slice`, `replan-slice`, `reassess-roadmap`, `run-uat`.
179
+
115
180
  ---
116
181
 
117
182
  ## Best Practices
@@ -277,3 +342,137 @@ git:
277
342
  ```
278
343
 
279
344
  All git fields are optional. Omit any field to use the default behavior. Project-level preferences override global preferences on a per-field basis.
345
+
346
+ ---
347
+
348
+ ## Budget & Cost Control Example
349
+
350
+ ```yaml
351
+ ---
352
+ version: 1
353
+ budget_ceiling: 10.00
354
+ budget_enforcement: pause
355
+ context_pause_threshold: 80
356
+ ---
357
+ ```
358
+
359
+ Sets a $10 budget ceiling. Auto-mode pauses when the ceiling is reached. Context window pauses at 80% usage for checkpointing.
360
+
361
+ ---
362
+
363
+ ## Notifications Example
364
+
365
+ ```yaml
366
+ ---
367
+ version: 1
368
+ notifications:
369
+ enabled: true
370
+ on_complete: false
371
+ on_error: true
372
+ on_budget: true
373
+ on_milestone: true
374
+ on_attention: true
375
+ ---
376
+ ```
377
+
378
+ Disables per-unit completion notifications (noisy in long runs) while keeping error, budget, milestone, and attention notifications enabled.
379
+
380
+ ---
381
+
382
+ ## Post-Unit Hooks Example
383
+
384
+ ```yaml
385
+ ---
386
+ version: 1
387
+ post_unit_hooks:
388
+ - name: code-review
389
+ after:
390
+ - execute-task
391
+ prompt: "Review the code changes in {sliceId}/{taskId} for quality, security, and test coverage."
392
+ max_cycles: 1
393
+ artifact: REVIEW.md
394
+ ---
395
+ ```
396
+
397
+ Runs an automated code review after each task execution. Skips if `REVIEW.md` already exists (idempotent).
398
+
399
+ ---
400
+
401
+ ## Pre-Dispatch Hooks Examples
402
+
403
+ **Modify — inject instructions before every task:**
404
+
405
+ ```yaml
406
+ ---
407
+ version: 1
408
+ pre_dispatch_hooks:
409
+ - name: enforce-standards
410
+ before:
411
+ - execute-task
412
+ action: modify
413
+ prepend: "Follow our TypeScript coding standards and always run linting."
414
+ ---
415
+ ```
416
+
417
+ **Skip — skip per-slice research when a research file already exists:**
418
+
419
+ ```yaml
420
+ ---
421
+ version: 1
422
+ pre_dispatch_hooks:
423
+ - name: skip-existing-research
424
+ before:
425
+ - research-slice
426
+ action: skip
427
+ skip_if: RESEARCH.md
428
+ ---
429
+ ```
430
+
431
+ **Replace — substitute a custom prompt for task execution:**
432
+
433
+ ```yaml
434
+ ---
435
+ version: 1
436
+ pre_dispatch_hooks:
437
+ - name: tdd-execute
438
+ before:
439
+ - execute-task
440
+ action: replace
441
+ prompt: "Implement the task using strict TDD. Write failing tests first, then implement, then refactor."
442
+ model: claude-opus-4-6
443
+ ---
444
+ ```
445
+
446
+ ---
447
+
448
+ ## Token Profile & Phases Example
449
+
450
+ ```yaml
451
+ ---
452
+ version: 1
453
+ token_profile: budget
454
+ phases:
455
+ skip_research: true
456
+ skip_reassess: true
457
+ skip_slice_research: false
458
+ ---
459
+ ```
460
+
461
+ Uses the `budget` profile to minimize token usage, with explicit override to keep slice-level research enabled.
462
+
463
+ ---
464
+
465
+ ## Remote Questions Example
466
+
467
+ ```yaml
468
+ ---
469
+ version: 1
470
+ remote_questions:
471
+ channel: slack
472
+ channel_id: "C0123456789"
473
+ timeout_minutes: 15
474
+ poll_interval_seconds: 10
475
+ ---
476
+ ```
477
+
478
+ Routes interactive questions to a Slack channel for headless auto-mode sessions. Questions time out after 15 minutes if unanswered.
@@ -849,7 +849,7 @@ export function parseContextDependsOn(content: string | null): string[] {
849
849
  const fm = parseFrontmatterMap(fmLines);
850
850
  const raw = fm['depends_on'];
851
851
  if (!Array.isArray(raw) || raw.length === 0) return [];
852
- return (raw as string[]).map(s => String(s).toUpperCase().trim()).filter(Boolean);
852
+ return (raw as string[]).map(s => String(s).trim()).filter(Boolean);
853
853
  }
854
854
 
855
855
  /**
@@ -951,6 +951,128 @@ export async function appendOverride(basePath: string, change: string, appliedAt
951
951
  }
952
952
  }
953
953
 
954
+ export async function appendKnowledge(
955
+ basePath: string,
956
+ type: "rule" | "pattern" | "lesson",
957
+ entry: string,
958
+ scope: string,
959
+ ): Promise<void> {
960
+ const knowledgePath = resolveGsdRootFile(basePath, "KNOWLEDGE");
961
+ const existing = await loadFile(knowledgePath);
962
+
963
+ if (existing) {
964
+ // Find the next ID for this type
965
+ const prefix = type === "rule" ? "K" : type === "pattern" ? "P" : "L";
966
+ const idPattern = new RegExp(`^\\| ${prefix}(\\d+)`, "gm");
967
+ let maxId = 0;
968
+ let match;
969
+ while ((match = idPattern.exec(existing)) !== null) {
970
+ const num = parseInt(match[1], 10);
971
+ if (num > maxId) maxId = num;
972
+ }
973
+ const nextId = `${prefix}${String(maxId + 1).padStart(3, "0")}`;
974
+
975
+ // Build the table row
976
+ let row: string;
977
+ if (type === "rule") {
978
+ row = `| ${nextId} | ${scope} | ${entry} | — | manual |`;
979
+ } else if (type === "pattern") {
980
+ row = `| ${nextId} | ${entry} | — | ${scope} |`;
981
+ } else {
982
+ row = `| ${nextId} | ${entry} | — | — | ${scope} |`;
983
+ }
984
+
985
+ // Find the right section and append after the table header
986
+ const sectionHeading = type === "rule" ? "## Rules" : type === "pattern" ? "## Patterns" : "## Lessons Learned";
987
+ const sectionIdx = existing.indexOf(sectionHeading);
988
+ if (sectionIdx !== -1) {
989
+ // Find the end of the table header row (the |---|...| line)
990
+ const afterHeading = existing.indexOf("\n", sectionIdx);
991
+ // Find the next section or end
992
+ const nextSection = existing.indexOf("\n## ", afterHeading + 1);
993
+ const insertPoint = nextSection !== -1 ? nextSection : existing.length;
994
+
995
+ // Insert row before the next section (or at end)
996
+ const before = existing.slice(0, insertPoint).trimEnd();
997
+ const after = existing.slice(insertPoint);
998
+ await saveFile(knowledgePath, before + "\n" + row + "\n" + after);
999
+ } else {
1000
+ // Section not found — append at end
1001
+ await saveFile(knowledgePath, existing.trimEnd() + "\n\n" + row + "\n");
1002
+ }
1003
+ } else {
1004
+ // Create file from scratch with template header
1005
+ const header = [
1006
+ "# Project Knowledge",
1007
+ "",
1008
+ "Append-only register of project-specific rules, patterns, and lessons learned.",
1009
+ "Agents read this before every unit. Add entries when you discover something worth remembering.",
1010
+ "",
1011
+ ].join("\n");
1012
+
1013
+ let content: string;
1014
+ if (type === "rule") {
1015
+ content = header + [
1016
+ "## Rules",
1017
+ "",
1018
+ "| # | Scope | Rule | Why | Added |",
1019
+ "|---|-------|------|-----|-------|",
1020
+ `| K001 | ${scope} | ${entry} | — | manual |`,
1021
+ "",
1022
+ "## Patterns",
1023
+ "",
1024
+ "| # | Pattern | Where | Notes |",
1025
+ "|---|---------|-------|-------|",
1026
+ "",
1027
+ "## Lessons Learned",
1028
+ "",
1029
+ "| # | What Happened | Root Cause | Fix | Scope |",
1030
+ "|---|--------------|------------|-----|-------|",
1031
+ "",
1032
+ ].join("\n");
1033
+ } else if (type === "pattern") {
1034
+ content = header + [
1035
+ "## Rules",
1036
+ "",
1037
+ "| # | Scope | Rule | Why | Added |",
1038
+ "|---|-------|------|-----|-------|",
1039
+ "",
1040
+ "## Patterns",
1041
+ "",
1042
+ "| # | Pattern | Where | Notes |",
1043
+ "|---|---------|-------|-------|",
1044
+ `| P001 | ${entry} | — | ${scope} |`,
1045
+ "",
1046
+ "## Lessons Learned",
1047
+ "",
1048
+ "| # | What Happened | Root Cause | Fix | Scope |",
1049
+ "|---|--------------|------------|-----|-------|",
1050
+ "",
1051
+ ].join("\n");
1052
+ } else {
1053
+ content = header + [
1054
+ "## Rules",
1055
+ "",
1056
+ "| # | Scope | Rule | Why | Added |",
1057
+ "|---|-------|------|-----|-------|",
1058
+ "",
1059
+ "## Patterns",
1060
+ "",
1061
+ "| # | Pattern | Where | Notes |",
1062
+ "|---|---------|-------|-------|",
1063
+ "",
1064
+ "## Lessons Learned",
1065
+ "",
1066
+ "| # | What Happened | Root Cause | Fix | Scope |",
1067
+ "|---|--------------|------------|-----|-------|",
1068
+ `| L001 | ${entry} | — | — | ${scope} |`,
1069
+ "",
1070
+ ].join("\n");
1071
+ }
1072
+ await saveFile(knowledgePath, content);
1073
+ }
1074
+ }
1075
+
954
1076
  export async function loadActiveOverrides(basePath: string): Promise<Override[]> {
955
1077
  const overridesPath = resolveGsdRootFile(basePath, "OVERRIDES");
956
1078
  const content = await loadFile(overridesPath);
@@ -22,11 +22,12 @@ import {
22
22
  } from "./paths.js";
23
23
  import { randomInt } from "node:crypto";
24
24
  import { join } from "node:path";
25
- import { readFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
25
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
26
26
  import { nativeIsRepo, nativeInit, nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
27
27
  import { ensureGitignore, ensurePreferences, untrackRuntimeFiles } from "./gitignore.js";
28
28
  import { loadEffectiveGSDPreferences } from "./preferences.js";
29
29
  import { showConfirm } from "../shared/confirm-ui.js";
30
+ import { loadQueueOrder, sortByQueueOrder, saveQueueOrder } from "./queue-order.js";
30
31
 
31
32
  // ─── Auto-start after discuss ─────────────────────────────────────────────────
32
33
 
@@ -203,13 +204,16 @@ function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string)
203
204
  export function findMilestoneIds(basePath: string): string[] {
204
205
  const dir = milestonesDir(basePath);
205
206
  try {
206
- return readdirSync(dir, { withFileTypes: true })
207
+ const ids = readdirSync(dir, { withFileTypes: true })
207
208
  .filter((d) => d.isDirectory())
208
209
  .map((d) => {
209
210
  const match = d.name.match(/^(M\d+(?:-[a-z0-9]{6})?)/);
210
211
  return match ? match[1] : d.name;
211
- })
212
- .sort(milestoneIdSort);
212
+ });
213
+
214
+ // Apply custom queue order if available, else fall back to numeric sort
215
+ const customOrder = loadQueueOrder(basePath);
216
+ return sortByQueueOrder(ids, customOrder);
213
217
  } catch {
214
218
  return [];
215
219
  }
@@ -305,6 +309,235 @@ export async function showQueue(
305
309
  return;
306
310
  }
307
311
 
312
+ // ── Count pending milestones ────────────────────────────────────────
313
+ const pendingMilestones = state.registry.filter(
314
+ m => m.status === "pending" || m.status === "active",
315
+ );
316
+ const completeCount = state.registry.filter(m => m.status === "complete").length;
317
+
318
+ // ── If multiple pending milestones, show queue management hub ──────
319
+ if (pendingMilestones.length > 1) {
320
+ const choice = await showNextAction(ctx, {
321
+ title: "GSD — Queue Management",
322
+ summary: [
323
+ `${completeCount} complete, ${pendingMilestones.length} pending.`,
324
+ ],
325
+ actions: [
326
+ {
327
+ id: "reorder",
328
+ label: "Reorder queue",
329
+ description: `Change execution order of ${pendingMilestones.length} pending milestones.`,
330
+ recommended: true,
331
+ },
332
+ {
333
+ id: "add",
334
+ label: "Add new work",
335
+ description: "Queue new milestones via discussion.",
336
+ },
337
+ ],
338
+ notYetMessage: "Run /gsd queue when ready.",
339
+ });
340
+
341
+ if (choice === "reorder") {
342
+ await handleQueueReorder(ctx, basePath, state);
343
+ return;
344
+ }
345
+ if (choice === "not_yet") return;
346
+ // "add" falls through to existing queue-add logic below
347
+ }
348
+
349
+ // ── Existing queue-add flow ─────────────────────────────────────────
350
+ await showQueueAdd(ctx, pi, basePath, state);
351
+ }
352
+
353
+ async function handleQueueReorder(
354
+ ctx: ExtensionCommandContext,
355
+ basePath: string,
356
+ state: Awaited<ReturnType<typeof deriveState>>,
357
+ ): Promise<void> {
358
+ const { showQueueReorder: showReorderUI } = await import("./queue-reorder-ui.js");
359
+ const { invalidateStateCache } = await import("./state.js");
360
+
361
+ const completed = state.registry
362
+ .filter(m => m.status === "complete")
363
+ .map(m => ({ id: m.id, title: m.title, dependsOn: m.dependsOn }));
364
+
365
+ const pending = state.registry
366
+ .filter(m => m.status !== "complete")
367
+ .map(m => ({ id: m.id, title: m.title, dependsOn: m.dependsOn }));
368
+
369
+ const result = await showReorderUI(ctx, completed, pending);
370
+ if (!result) {
371
+ ctx.ui.notify("Queue reorder cancelled.", "info");
372
+ return;
373
+ }
374
+
375
+ // Save the new order
376
+ saveQueueOrder(basePath, result.order);
377
+ invalidateStateCache();
378
+
379
+ // Remove conflicting depends_on entries from CONTEXT.md files
380
+ if (result.depsToRemove.length > 0) {
381
+ removeDependsOnFromContextFiles(basePath, result.depsToRemove);
382
+ }
383
+
384
+ // Sync PROJECT.md milestone sequence table
385
+ syncProjectMdSequence(basePath, state.registry, result.order);
386
+
387
+ // Commit the change
388
+ const filesToAdd = [".gsd/QUEUE-ORDER.json", ".gsd/PROJECT.md"];
389
+ for (const r of result.depsToRemove) {
390
+ filesToAdd.push(`.gsd/milestones/${r.milestone}/${r.milestone}-CONTEXT.md`);
391
+ }
392
+ try {
393
+ nativeAddPaths(basePath, filesToAdd);
394
+ nativeCommit(basePath, "docs: reorder queue");
395
+ } catch {
396
+ // Commit may fail if nothing changed or git hooks block — non-fatal
397
+ }
398
+
399
+ const depInfo = result.depsToRemove.length > 0
400
+ ? ` (removed ${result.depsToRemove.length} depends_on)`
401
+ : "";
402
+ ctx.ui.notify(`Queue reordered: ${result.order.join(" → ")}${depInfo}`, "info");
403
+ }
404
+
405
+ /**
406
+ * Remove specific depends_on entries from milestone CONTEXT.md frontmatter.
407
+ */
408
+ function removeDependsOnFromContextFiles(
409
+ basePath: string,
410
+ depsToRemove: Array<{ milestone: string; dep: string }>,
411
+ ): void {
412
+ // Group removals by milestone
413
+ const byMilestone = new Map<string, string[]>();
414
+ for (const { milestone, dep } of depsToRemove) {
415
+ const existing = byMilestone.get(milestone) ?? [];
416
+ existing.push(dep);
417
+ byMilestone.set(milestone, existing);
418
+ }
419
+
420
+ for (const [mid, depsToRemoveForMid] of byMilestone) {
421
+ const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
422
+ if (!contextFile || !existsSync(contextFile)) continue;
423
+
424
+ const content = readFileSync(contextFile, "utf-8");
425
+
426
+ // Parse frontmatter
427
+ const trimmed = content.trimStart();
428
+ if (!trimmed.startsWith("---")) continue;
429
+ const afterFirst = trimmed.indexOf("\n");
430
+ if (afterFirst === -1) continue;
431
+ const rest = trimmed.slice(afterFirst + 1);
432
+ const endIdx = rest.indexOf("\n---");
433
+ if (endIdx === -1) continue;
434
+
435
+ const fmText = rest.slice(0, endIdx);
436
+ const body = rest.slice(endIdx + 4);
437
+
438
+ // Parse depends_on line(s)
439
+ const fmLines = fmText.split("\n");
440
+ const removeSet = new Set(depsToRemoveForMid.map(d => d.toUpperCase()));
441
+
442
+ // Handle inline format: depends_on: [M009, M010]
443
+ const inlineMatch = fmLines.findIndex(l => /^depends_on:\s*\[/.test(l));
444
+ if (inlineMatch >= 0) {
445
+ const line = fmLines[inlineMatch];
446
+ const inner = line.match(/\[([^\]]*)\]/);
447
+ if (inner) {
448
+ const remaining = inner[1]
449
+ .split(",")
450
+ .map(s => s.trim())
451
+ .filter(s => s && !removeSet.has(s.toUpperCase()));
452
+ if (remaining.length === 0) {
453
+ fmLines.splice(inlineMatch, 1);
454
+ } else {
455
+ fmLines[inlineMatch] = `depends_on: [${remaining.join(", ")}]`;
456
+ }
457
+ }
458
+ } else {
459
+ // Handle multi-line format
460
+ const keyIdx = fmLines.findIndex(l => /^depends_on:\s*$/.test(l));
461
+ if (keyIdx >= 0) {
462
+ let end = keyIdx + 1;
463
+ while (end < fmLines.length && /^\s+-\s/.test(fmLines[end])) {
464
+ const val = fmLines[end].replace(/^\s+-\s*/, "").trim().toUpperCase();
465
+ if (removeSet.has(val)) {
466
+ fmLines.splice(end, 1);
467
+ } else {
468
+ end++;
469
+ }
470
+ }
471
+ if (end === keyIdx + 1 || (end <= fmLines.length && !/^\s+-\s/.test(fmLines[keyIdx + 1] ?? ""))) {
472
+ fmLines.splice(keyIdx, 1);
473
+ }
474
+ }
475
+ }
476
+
477
+ // Rebuild file
478
+ const newFm = fmLines.filter(l => l !== undefined).join("\n");
479
+ const newContent = newFm.trim()
480
+ ? `---\n${newFm}\n---${body}`
481
+ : body.replace(/^\n+/, "");
482
+ writeFileSync(contextFile, newContent, "utf-8");
483
+ }
484
+ }
485
+
486
+ function syncProjectMdSequence(
487
+ basePath: string,
488
+ registry: Array<{ id: string; title: string; status: string }>,
489
+ newOrder: string[],
490
+ ): void {
491
+ const projectPath = resolveGsdRootFile(basePath, "PROJECT");
492
+ if (!projectPath || !existsSync(projectPath)) return;
493
+
494
+ const content = readFileSync(projectPath, "utf-8");
495
+ const lines = content.split("\n");
496
+
497
+ const headerIdx = lines.findIndex(l => /^##\s+Milestone Sequence/.test(l));
498
+ if (headerIdx < 0) return;
499
+
500
+ let tableStart = headerIdx + 1;
501
+ while (tableStart < lines.length && !lines[tableStart].startsWith("|")) tableStart++;
502
+ if (tableStart >= lines.length) return;
503
+
504
+ let tableEnd = tableStart + 1;
505
+ while (tableEnd < lines.length && lines[tableEnd].startsWith("|")) tableEnd++;
506
+
507
+ const registryMap = new Map(registry.map(m => [m.id, m]));
508
+ const completedSet = new Set(registry.filter(m => m.status === "complete").map(m => m.id));
509
+
510
+ const newRows: string[] = [];
511
+ for (const m of registry) {
512
+ if (m.status === "complete") {
513
+ newRows.push(`| ${m.id} | ${m.title} | ✅ Complete |`);
514
+ }
515
+ }
516
+ let isFirst = true;
517
+ for (const id of newOrder) {
518
+ if (completedSet.has(id)) continue;
519
+ const m = registryMap.get(id);
520
+ if (!m) continue;
521
+ const status = isFirst ? "📋 Next" : "📋 Queued";
522
+ newRows.push(`| ${m.id} | ${m.title} | ${status} |`);
523
+ isFirst = false;
524
+ }
525
+
526
+ const headerLine = lines[tableStart];
527
+ const separatorLine = lines[tableStart + 1];
528
+ const newTable = [headerLine, separatorLine, ...newRows];
529
+ lines.splice(tableStart, tableEnd - tableStart, ...newTable);
530
+ writeFileSync(projectPath, lines.join("\n"), "utf-8");
531
+ }
532
+
533
+ async function showQueueAdd(
534
+ ctx: ExtensionCommandContext,
535
+ pi: ExtensionAPI,
536
+ basePath: string,
537
+ state: Awaited<ReturnType<typeof deriveState>>,
538
+ ): Promise<void> {
539
+ const milestoneIds = findMilestoneIds(basePath);
540
+
308
541
  // ── Build existing milestones context for the prompt ────────────────
309
542
  const existingContext = await buildExistingMilestonesContext(basePath, milestoneIds, state);
310
543