gsd-pi 2.27.0 → 2.28.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 (124) hide show
  1. package/README.md +16 -12
  2. package/dist/headless-query.d.ts +36 -0
  3. package/dist/headless-query.js +59 -0
  4. package/dist/headless.js +7 -4
  5. package/dist/help-text.js +2 -0
  6. package/dist/resources/extensions/gsd/auto.ts +11 -3
  7. package/dist/resources/extensions/gsd/commands.ts +55 -3
  8. package/dist/resources/extensions/gsd/crash-recovery.ts +5 -2
  9. package/dist/resources/extensions/gsd/docs/preferences-reference.md +83 -0
  10. package/dist/resources/extensions/gsd/export.ts +90 -27
  11. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  12. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  13. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  14. package/dist/resources/extensions/gsd/prompts/system.md +4 -1
  15. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +42 -15
  16. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +7 -2
  17. package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +4 -13
  18. package/dist/resources/extensions/gsd/templates/preferences.md +34 -0
  19. package/dist/resources/extensions/gsd/tests/export-html-all.test.ts +105 -0
  20. package/dist/resources/extensions/gsd/tests/headless-query.test.ts +162 -0
  21. package/dist/resources/extensions/gsd/tests/update-command.test.ts +67 -0
  22. package/dist/resources/extensions/subagent/index.ts +1 -1
  23. package/dist/update-check.js +1 -1
  24. package/package.json +2 -1
  25. package/packages/pi-ai/dist/utils/oauth/anthropic.d.ts.map +1 -1
  26. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -0
  27. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  28. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  29. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +5 -1
  30. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  31. package/packages/pi-ai/dist/utils/oauth/google-antigravity.d.ts.map +1 -1
  32. package/packages/pi-ai/dist/utils/oauth/google-antigravity.js +4 -0
  33. package/packages/pi-ai/dist/utils/oauth/google-antigravity.js.map +1 -1
  34. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.d.ts.map +1 -1
  35. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.js +6 -0
  36. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.js.map +1 -1
  37. package/packages/pi-ai/dist/utils/oauth/index.js +2 -2
  38. package/packages/pi-ai/dist/utils/oauth/index.js.map +1 -1
  39. package/packages/pi-ai/dist/utils/oauth/openai-codex.d.ts.map +1 -1
  40. package/packages/pi-ai/dist/utils/oauth/openai-codex.js +2 -0
  41. package/packages/pi-ai/dist/utils/oauth/openai-codex.js.map +1 -1
  42. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -0
  43. package/packages/pi-ai/src/utils/oauth/github-copilot.ts +5 -1
  44. package/packages/pi-ai/src/utils/oauth/google-antigravity.ts +4 -0
  45. package/packages/pi-ai/src/utils/oauth/google-gemini-cli.ts +6 -0
  46. package/packages/pi-ai/src/utils/oauth/index.ts +2 -2
  47. package/packages/pi-ai/src/utils/oauth/openai-codex.ts +2 -0
  48. package/packages/pi-coding-agent/dist/core/bash-executor.d.ts.map +1 -1
  49. package/packages/pi-coding-agent/dist/core/bash-executor.js +23 -1
  50. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  51. package/packages/pi-coding-agent/dist/core/blob-store.d.ts +8 -0
  52. package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -1
  53. package/packages/pi-coding-agent/dist/core/blob-store.js +50 -1
  54. package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -1
  55. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  56. package/packages/pi-coding-agent/dist/core/extensions/runner.js +32 -10
  57. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  58. package/packages/pi-coding-agent/dist/core/extensions/runner.test.d.ts +2 -0
  59. package/packages/pi-coding-agent/dist/core/extensions/runner.test.d.ts.map +1 -0
  60. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +77 -0
  61. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -0
  62. package/packages/pi-coding-agent/dist/core/fs-utils.d.ts +7 -0
  63. package/packages/pi-coding-agent/dist/core/fs-utils.d.ts.map +1 -0
  64. package/packages/pi-coding-agent/dist/core/fs-utils.js +12 -0
  65. package/packages/pi-coding-agent/dist/core/fs-utils.js.map +1 -0
  66. package/packages/pi-coding-agent/dist/core/fs-utils.test.d.ts +2 -0
  67. package/packages/pi-coding-agent/dist/core/fs-utils.test.d.ts.map +1 -0
  68. package/packages/pi-coding-agent/dist/core/fs-utils.test.js +67 -0
  69. package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -0
  70. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/lsp/client.js +22 -2
  72. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +7 -0
  74. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/session-manager.js +83 -12
  76. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/index.d.ts +2 -1
  78. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/index.js +3 -1
  80. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  82. package/packages/pi-coding-agent/dist/main.js +6 -0
  83. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +8 -1
  86. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +11 -0
  89. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
  90. package/packages/pi-coding-agent/src/core/bash-executor.ts +23 -1
  91. package/packages/pi-coding-agent/src/core/blob-store.ts +46 -1
  92. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +84 -0
  93. package/packages/pi-coding-agent/src/core/extensions/runner.ts +31 -10
  94. package/packages/pi-coding-agent/src/core/fs-utils.test.ts +66 -0
  95. package/packages/pi-coding-agent/src/core/fs-utils.ts +12 -0
  96. package/packages/pi-coding-agent/src/core/lsp/client.ts +22 -2
  97. package/packages/pi-coding-agent/src/core/session-manager.ts +84 -12
  98. package/packages/pi-coding-agent/src/index.ts +9 -0
  99. package/packages/pi-coding-agent/src/main.ts +7 -0
  100. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +9 -1
  101. package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +12 -0
  102. package/src/resources/extensions/gsd/auto.ts +11 -3
  103. package/src/resources/extensions/gsd/commands.ts +55 -3
  104. package/src/resources/extensions/gsd/crash-recovery.ts +5 -2
  105. package/src/resources/extensions/gsd/docs/preferences-reference.md +83 -0
  106. package/src/resources/extensions/gsd/export.ts +90 -27
  107. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  108. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  109. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  110. package/src/resources/extensions/gsd/prompts/system.md +4 -1
  111. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +42 -15
  112. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +7 -2
  113. package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +4 -13
  114. package/src/resources/extensions/gsd/templates/preferences.md +34 -0
  115. package/src/resources/extensions/gsd/tests/export-html-all.test.ts +105 -0
  116. package/src/resources/extensions/gsd/tests/headless-query.test.ts +162 -0
  117. package/src/resources/extensions/gsd/tests/update-command.test.ts +67 -0
  118. package/src/resources/extensions/subagent/index.ts +1 -1
  119. package/dist/resources/extensions/gsd/mcp-server.ts +0 -108
  120. package/dist/resources/extensions/gsd/tests/marketplace-discovery.test.ts +0 -202
  121. package/dist/resources/extensions/shared/bundled-extension-paths.ts +0 -11
  122. package/src/resources/extensions/gsd/mcp-server.ts +0 -108
  123. package/src/resources/extensions/gsd/tests/marketplace-discovery.test.ts +0 -202
  124. package/src/resources/extensions/shared/bundled-extension-paths.ts +0 -11
package/README.md CHANGED
@@ -141,21 +141,23 @@ Auto mode is a state machine driven by files on disk. It reads `.gsd/STATE.md`,
141
141
 
142
142
  3. **Git worktree isolation** — Each milestone runs in its own git worktree with a `milestone/<MID>` branch. All slice work commits sequentially — no branch switching, no merge conflicts. When the milestone completes, it's squash-merged to main as one clean commit.
143
143
 
144
- 4. **Crash recovery** — A lock file tracks the current unit. If the session dies, the next `/gsd auto` reads the surviving session file, synthesizes a recovery briefing from every tool call that made it to disk, and resumes with full context. Parallel orchestrator state is persisted to disk with PID liveness detection, so multi-worker sessions survive crashes too.
144
+ 4. **Crash recovery** — A lock file tracks the current unit. If the session dies, the next `/gsd auto` reads the surviving session file, synthesizes a recovery briefing from every tool call that made it to disk, and resumes with full context. Parallel orchestrator state is persisted to disk with PID liveness detection, so multi-worker sessions survive crashes too. In headless mode, crashes trigger automatic restart with exponential backoff (default 3 attempts).
145
145
 
146
- 5. **Stuck detection** — If the same unit dispatches twice (the LLM didn't produce the expected artifact), it retries once with a deep diagnostic. If it fails again, auto mode stops with the exact file it expected.
146
+ 5. **Provider error recovery** — Transient provider errors (rate limits, 500/503 server errors, overloaded) auto-resume after a delay. Permanent errors (auth, billing) pause for manual review. The model fallback chain retries transient network errors before switching models.
147
147
 
148
- 6. **Timeout supervision** — Soft timeout warns the LLM to wrap up. Idle watchdog detects stalls. Hard timeout pauses auto mode. Recovery steering nudges the LLM to finish durable output before giving up.
148
+ 6. **Stuck detection** — If the same unit dispatches twice (the LLM didn't produce the expected artifact), it retries once with a deep diagnostic. If it fails again, auto mode stops with the exact file it expected.
149
149
 
150
- 7. **Cost tracking** — Every unit's token usage and cost is captured, broken down by phase, slice, and model. The dashboard shows running totals and projections. Budget ceilings can pause auto mode before overspending.
150
+ 7. **Timeout supervision** — Soft timeout warns the LLM to wrap up. Idle watchdog detects stalls. Hard timeout pauses auto mode. Recovery steering nudges the LLM to finish durable output before giving up.
151
151
 
152
- 8. **Adaptive replanning** — After each slice completes, the roadmap is reassessed. If the work revealed new information that changes the plan, slices are reordered, added, or removed before continuing.
152
+ 8. **Cost tracking** — Every unit's token usage and cost is captured, broken down by phase, slice, and model. The dashboard shows running totals and projections. Budget ceilings can pause auto mode before overspending.
153
153
 
154
- 9. **Verification enforcement** — Configure shell commands (`npm run lint`, `npm run test`, etc.) that run automatically after task execution. Failures trigger auto-fix retries before advancing. Configurable via `verification_commands`, `verification_auto_fix`, and `verification_max_retries` preferences.
154
+ 9. **Adaptive replanning** — After each slice completes, the roadmap is reassessed. If the work revealed new information that changes the plan, slices are reordered, added, or removed before continuing.
155
155
 
156
- 10. **Milestone validation** — After all slices complete, a `validate-milestone` gate compares roadmap success criteria against actual results before sealing the milestone.
156
+ 10. **Verification enforcement** — Configure shell commands (`npm run lint`, `npm run test`, etc.) that run automatically after task execution. Failures trigger auto-fix retries before advancing. Configurable via `verification_commands`, `verification_auto_fix`, and `verification_max_retries` preferences.
157
157
 
158
- 11. **Escape hatch** — Press Escape to pause. The conversation is preserved. Interact with the agent, inspect what happened, or just `/gsd auto` to resume from disk state.
158
+ 11. **Milestone validation** — After all slices complete, a `validate-milestone` gate compares roadmap success criteria against actual results before sealing the milestone.
159
+
160
+ 12. **Escape hatch** — Press Escape to pause. The conversation is preserved. Interact with the agent, inspect what happened, or just `/gsd auto` to resume from disk state.
159
161
 
160
162
  ### `/gsd` and `/gsd next` — Step Mode
161
163
 
@@ -247,14 +249,14 @@ gsd headless new-milestone --context spec.md --auto
247
249
  # One unit at a time (cron-friendly)
248
250
  gsd headless next
249
251
 
250
- # Machine-readable JSONL event stream
251
- gsd headless --json status
252
+ # Instant JSON snapshot (no LLM, ~50ms)
253
+ gsd headless query
252
254
 
253
255
  # Force a specific pipeline phase
254
256
  gsd headless dispatch plan
255
257
  ```
256
258
 
257
- Headless auto-responds to interactive prompts, detects completion, and exits with structured codes: `0` complete, `1` error/timeout, `2` blocked. Auto-restarts on crash with exponential backoff. Pair with [remote questions](./docs/remote-questions.md) to route decisions to Slack or Discord when human input is needed.
259
+ Headless auto-responds to interactive prompts, detects completion, and exits with structured codes: `0` complete, `1` error/timeout, `2` blocked. Auto-restarts on crash with exponential backoff. Use `gsd headless query` for instant, machine-readable state inspection — returns phase, next dispatch preview, and parallel worker costs as a single JSON object without spawning an LLM session. Pair with [remote questions](./docs/remote-questions.md) to route decisions to Slack or Discord when human input is needed.
258
260
 
259
261
  **Multi-session orchestration** — headless mode supports file-based IPC in `.gsd/parallel/` for coordinating multiple GSD workers across milestones. Build orchestrators that spawn, monitor, and budget-cap a fleet of GSD workers.
260
262
 
@@ -295,6 +297,7 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
295
297
  | `gsd config` | Re-run the setup wizard (LLM provider + tool keys) |
296
298
  | `gsd update` | Update GSD to the latest version |
297
299
  | `gsd headless [cmd]` | Run `/gsd` commands without TUI (CI, cron, scripts) |
300
+ | `gsd headless query` | Instant JSON snapshot — state, next dispatch, costs (no LLM) |
298
301
  | `gsd --continue` (`-c`) | Resume the most recent session for the current directory |
299
302
  | `gsd sessions` | Interactive session picker — browse and resume any saved session |
300
303
 
@@ -414,7 +417,8 @@ auto_report: true
414
417
  | `skill_rules` | Situational rules for skill routing |
415
418
  | `skill_staleness_days` | Skills unused for N days get deprioritized (default: 60, 0 = disabled) |
416
419
  | `unique_milestone_ids` | Uses unique milestone names to avoid clashes when working in teams of people |
417
- | `git.isolation` | `worktree` (default) or `none` — disable worktree isolation for projects that don't need it |
420
+ | `git.isolation` | `worktree` (default), `branch`, or `none` — disable worktree isolation for projects that don't need it |
421
+ | `git.manage_gitignore` | Set `false` to prevent GSD from modifying `.gitignore` |
418
422
  | `verification_commands`| Array of shell commands to run after task execution (e.g., `["npm run lint", "npm run test"]`) |
419
423
  | `verification_auto_fix`| Auto-retry on verification failures (default: true) |
420
424
  | `verification_max_retries` | Max retries for verification failures (default: 2) |
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Headless Query — `gsd headless query`
3
+ *
4
+ * Single read-only command that returns the full project snapshot as JSON
5
+ * to stdout, without spawning an LLM session. Instant (~50ms).
6
+ *
7
+ * Output: { state, next, cost }
8
+ * state — deriveState() output (phase, milestones, progress, blockers)
9
+ * next — dry-run dispatch preview (what auto-mode would do next)
10
+ * cost — aggregated parallel worker costs
11
+ */
12
+ import type { GSDState } from './resources/extensions/gsd/types.js';
13
+ export interface QuerySnapshot {
14
+ state: GSDState;
15
+ next: {
16
+ action: 'dispatch' | 'stop' | 'skip';
17
+ unitType?: string;
18
+ unitId?: string;
19
+ reason?: string;
20
+ };
21
+ cost: {
22
+ workers: Array<{
23
+ milestoneId: string;
24
+ pid: number;
25
+ state: string;
26
+ cost: number;
27
+ lastHeartbeat: number;
28
+ }>;
29
+ total: number;
30
+ };
31
+ }
32
+ export interface QueryResult {
33
+ exitCode: number;
34
+ data?: QuerySnapshot;
35
+ }
36
+ export declare function handleQuery(basePath: string): Promise<QueryResult>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Headless Query — `gsd headless query`
3
+ *
4
+ * Single read-only command that returns the full project snapshot as JSON
5
+ * to stdout, without spawning an LLM session. Instant (~50ms).
6
+ *
7
+ * Output: { state, next, cost }
8
+ * state — deriveState() output (phase, milestones, progress, blockers)
9
+ * next — dry-run dispatch preview (what auto-mode would do next)
10
+ * cost — aggregated parallel worker costs
11
+ */
12
+ import { deriveState } from './resources/extensions/gsd/state.js';
13
+ import { resolveDispatch } from './resources/extensions/gsd/auto-dispatch.js';
14
+ import { readAllSessionStatuses } from './resources/extensions/gsd/session-status-io.js';
15
+ import { loadEffectiveGSDPreferences } from './resources/extensions/gsd/preferences.js';
16
+ // ─── Implementation ─────────────────────────────────────────────────────────
17
+ export async function handleQuery(basePath) {
18
+ const state = await deriveState(basePath);
19
+ // Derive next dispatch action
20
+ let next;
21
+ if (!state.activeMilestone) {
22
+ next = {
23
+ action: 'stop',
24
+ reason: state.phase === 'complete' ? 'All milestones complete.' : state.nextAction,
25
+ };
26
+ }
27
+ else {
28
+ const loaded = loadEffectiveGSDPreferences();
29
+ const dispatch = await resolveDispatch({
30
+ basePath,
31
+ mid: state.activeMilestone.id,
32
+ midTitle: state.activeMilestone.title,
33
+ state,
34
+ prefs: loaded?.preferences,
35
+ });
36
+ next = {
37
+ action: dispatch.action,
38
+ unitType: dispatch.action === 'dispatch' ? dispatch.unitType : undefined,
39
+ unitId: dispatch.action === 'dispatch' ? dispatch.unitId : undefined,
40
+ reason: dispatch.action === 'stop' ? dispatch.reason : undefined,
41
+ };
42
+ }
43
+ // Aggregate parallel worker costs
44
+ const statuses = readAllSessionStatuses(basePath);
45
+ const workers = statuses.map((s) => ({
46
+ milestoneId: s.milestoneId,
47
+ pid: s.pid,
48
+ state: s.state,
49
+ cost: s.cost,
50
+ lastHeartbeat: s.lastHeartbeat,
51
+ }));
52
+ const snapshot = {
53
+ state,
54
+ next,
55
+ cost: { workers, total: workers.reduce((sum, w) => sum + w.cost, 0) },
56
+ };
57
+ process.stdout.write(JSON.stringify(snapshot) + '\n');
58
+ return { exitCode: 0, data: snapshot };
59
+ }
package/dist/headless.js CHANGED
@@ -12,10 +12,7 @@
12
12
  */
13
13
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
14
14
  import { join, resolve } from 'node:path';
15
- // RpcClient is not in @gsd/pi-coding-agent's public exports — import from dist directly.
16
- // This relative path resolves correctly from both src/ (via tsx) and dist/ (compiled).
17
- import { RpcClient } from '../packages/pi-coding-agent/dist/modes/rpc/rpc-client.js';
18
- import { attachJsonlLineReader, serializeJsonLine } from '../packages/pi-coding-agent/dist/modes/rpc/jsonl.js';
15
+ import { RpcClient, attachJsonlLineReader, serializeJsonLine } from '@gsd/pi-coding-agent';
19
16
  // ---------------------------------------------------------------------------
20
17
  // CLI Argument Parser
21
18
  // ---------------------------------------------------------------------------
@@ -336,6 +333,12 @@ async function runHeadlessOnce(options, restartCount) {
336
333
  process.stderr.write("[headless] Run 'gsd' interactively first to initialize a project.\n");
337
334
  process.exit(1);
338
335
  }
336
+ // Query: read-only state snapshot, no RPC child needed
337
+ if (options.command === 'query') {
338
+ const { handleQuery } = await import('./headless-query.js');
339
+ const result = await handleQuery(process.cwd());
340
+ return { exitCode: result.exitCode, interrupted: false };
341
+ }
339
342
  // Resolve CLI path for the child process
340
343
  const cliPath = process.env.GSD_BIN_PATH || process.argv[1];
341
344
  if (!cliPath) {
package/dist/help-text.js CHANGED
@@ -46,6 +46,7 @@ const SUBCOMMAND_HELP = {
46
46
  ' next Run one unit',
47
47
  ' status Show progress dashboard',
48
48
  ' new-milestone Create a milestone from a specification document',
49
+ ' query JSON snapshot: state + next dispatch + costs (no LLM)',
49
50
  '',
50
51
  'new-milestone flags:',
51
52
  ' --context <path> Path to spec/PRD file (use \'-\' for stdin)',
@@ -62,6 +63,7 @@ const SUBCOMMAND_HELP = {
62
63
  ' cat spec.md | gsd headless new-milestone --context - From stdin',
63
64
  ' gsd headless new-milestone --context spec.md --auto Create + auto-execute',
64
65
  ' gsd headless --supervised auto Supervised orchestrator mode',
66
+ ' gsd headless query Instant JSON state snapshot',
65
67
  '',
66
68
  'Exit codes: 0 = complete, 1 = error/timeout, 2 = blocked',
67
69
  ].join('\n'),
@@ -103,7 +103,7 @@ import {
103
103
  import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
104
104
  import { join } from "node:path";
105
105
  import { sep as pathSep } from "node:path";
106
- import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync, statSync } from "node:fs";
106
+ import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, unlinkSync, statSync } from "node:fs";
107
107
  import { nativeIsRepo, nativeInit, nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
108
108
  import {
109
109
  autoCommitCurrentBranch,
@@ -2138,7 +2138,11 @@ async function dispatchNextUnit(
2138
2138
  // Clear completed-units.json for the finished milestone
2139
2139
  try {
2140
2140
  const file = completedKeysPath(s.basePath);
2141
- if (existsSync(file)) writeFileSync(file, JSON.stringify([]), "utf-8");
2141
+ if (existsSync(file)) {
2142
+ const tmpFile = file + ".tmp";
2143
+ writeFileSync(tmpFile, JSON.stringify([]), "utf-8");
2144
+ renameSync(tmpFile, file);
2145
+ }
2142
2146
  s.completedKeySet.clear();
2143
2147
  } catch { /* non-fatal */ }
2144
2148
 
@@ -2286,7 +2290,11 @@ async function dispatchNextUnit(
2286
2290
  // Clear completed-units.json for the finished milestone so it doesn't grow unbounded.
2287
2291
  try {
2288
2292
  const file = completedKeysPath(s.basePath);
2289
- if (existsSync(file)) writeFileSync(file, JSON.stringify([]), "utf-8");
2293
+ if (existsSync(file)) {
2294
+ const tmpFile = file + ".tmp";
2295
+ writeFileSync(tmpFile, JSON.stringify([]), "utf-8");
2296
+ renameSync(tmpFile, file);
2297
+ }
2290
2298
  s.completedKeySet.clear();
2291
2299
  } catch { /* non-fatal */ }
2292
2300
  // ── Milestone merge: squash-merge milestone branch to main before stopping ──
@@ -77,7 +77,7 @@ function projectRoot(): string {
77
77
 
78
78
  export function registerGSDCommand(pi: ExtensionAPI): void {
79
79
  pi.registerCommand("gsd", {
80
- description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel",
80
+ description: "GSD — Get Shit Done: /gsd help|next|auto|stop|pause|status|visualize|queue|quick|capture|triage|dispatch|history|undo|skip|export|cleanup|mode|prefs|config|hooks|run-hook|skill-health|doctor|forensics|migrate|remote|steer|knowledge|new-milestone|parallel|update",
81
81
  getArgumentCompletions: (prefix: string) => {
82
82
  const subcommands = [
83
83
  { cmd: "help", desc: "Categorized command reference with descriptions" },
@@ -113,6 +113,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
113
113
  { cmd: "knowledge", desc: "Add persistent project knowledge (rule, pattern, or lesson)" },
114
114
  { cmd: "new-milestone", desc: "Create a milestone from a specification document (headless)" },
115
115
  { cmd: "parallel", desc: "Parallel milestone orchestration (start, status, stop, merge)" },
116
+ { cmd: "update", desc: "Update GSD to the latest version" },
116
117
  ];
117
118
  const parts = prefix.trim().split(/\s+/);
118
119
 
@@ -181,7 +182,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
181
182
 
182
183
  if (parts[0] === "export" && parts.length <= 2) {
183
184
  const flagPrefix = parts[1] ?? "";
184
- return ["--json", "--markdown"]
185
+ return ["--json", "--markdown", "--html", "--html --all"]
185
186
  .filter((f) => f.startsWith(flagPrefix))
186
187
  .map((f) => ({ value: `export ${f}`, label: f }));
187
188
  }
@@ -575,6 +576,11 @@ Examples:
575
576
  return;
576
577
  }
577
578
 
579
+ if (trimmed === "update") {
580
+ await handleUpdate(ctx);
581
+ return;
582
+ }
583
+
578
584
  if (trimmed === "") {
579
585
  // Bare /gsd defaults to step mode
580
586
  await startAuto(ctx, pi, projectRoot(), false, { step: true });
@@ -625,11 +631,12 @@ function showHelp(ctx: ExtensionCommandContext): void {
625
631
  "",
626
632
  "MAINTENANCE",
627
633
  " /gsd doctor Diagnose and repair .gsd/ state [audit|fix|heal] [scope]",
628
- " /gsd export Export milestone/slice results [--json|--markdown|--html]",
634
+ " /gsd export Export milestone/slice results [--json|--markdown|--html] [--all]",
629
635
  " /gsd cleanup Remove merged branches or snapshots [branches|snapshots]",
630
636
  " /gsd migrate Upgrade .gsd/ structures to new format",
631
637
  " /gsd remote Control remote auto-mode [slack|discord|status|disconnect]",
632
638
  " /gsd inspect Show SQLite DB diagnostics (schema, row counts, recent entries)",
639
+ " /gsd update Update GSD to the latest version via npm",
633
640
  ];
634
641
  ctx.ui.notify(lines.join("\n"), "info");
635
642
  }
@@ -2091,3 +2098,48 @@ Examples:
2091
2098
  ctx.ui.notify("Failed to dispatch hook. Auto-mode may have been cancelled.", "error");
2092
2099
  }
2093
2100
  }
2101
+
2102
+ // ─── Self-update handler ────────────────────────────────────────────────────
2103
+
2104
+ async function handleUpdate(ctx: ExtensionCommandContext): Promise<void> {
2105
+ const { execSync } = await import("node:child_process");
2106
+ const { compareSemver } = await import("../../../update-check.js");
2107
+
2108
+ const NPM_PACKAGE = "gsd-pi";
2109
+ const current = process.env.GSD_VERSION || "0.0.0";
2110
+
2111
+ ctx.ui.notify(`Current version: v${current}\nChecking npm registry...`, "info");
2112
+
2113
+ let latest: string;
2114
+ try {
2115
+ latest = execSync(`npm view ${NPM_PACKAGE} version`, {
2116
+ encoding: "utf-8",
2117
+ stdio: ["ignore", "pipe", "ignore"],
2118
+ }).trim();
2119
+ } catch {
2120
+ ctx.ui.notify("Failed to reach npm registry. Check your network connection.", "error");
2121
+ return;
2122
+ }
2123
+
2124
+ if (compareSemver(latest, current) <= 0) {
2125
+ ctx.ui.notify(`Already up to date (v${current}).`, "info");
2126
+ return;
2127
+ }
2128
+
2129
+ ctx.ui.notify(`Updating: v${current} → v${latest}...`, "info");
2130
+
2131
+ try {
2132
+ execSync(`npm install -g ${NPM_PACKAGE}@latest`, {
2133
+ stdio: ["ignore", "pipe", "ignore"],
2134
+ });
2135
+ ctx.ui.notify(
2136
+ `Updated to v${latest}. Restart your GSD session to use the new version.`,
2137
+ "info",
2138
+ );
2139
+ } catch {
2140
+ ctx.ui.notify(
2141
+ `Update failed. Try manually: npm install -g ${NPM_PACKAGE}@latest`,
2142
+ "error",
2143
+ );
2144
+ }
2145
+ }
@@ -10,7 +10,7 @@
10
10
  * so the file on disk reflects every tool call up to the crash point).
11
11
  */
12
12
 
13
- import { writeFileSync, readFileSync, unlinkSync, existsSync } from "node:fs";
13
+ import { renameSync, writeFileSync, readFileSync, unlinkSync, existsSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { gsdRoot } from "./paths.js";
16
16
 
@@ -49,7 +49,10 @@ export function writeLock(
49
49
  completedUnits,
50
50
  sessionFile,
51
51
  };
52
- writeFileSync(lockPath(basePath), JSON.stringify(data, null, 2), "utf-8");
52
+ const lp = lockPath(basePath);
53
+ const tmpLp = lp + ".tmp";
54
+ writeFileSync(tmpLp, JSON.stringify(data, null, 2), "utf-8");
55
+ renameSync(tmpLp, lp);
53
56
  } catch (e) { /* non-fatal: lock write failure */ void e; }
54
57
  }
55
58
 
@@ -104,6 +104,8 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
104
104
  - Object with provider: `{ model: "claude-opus-4-6", provider: "bedrock" }` — explicit provider targeting in object format
105
105
  - Omit a key to use whatever model is currently active. Fallbacks are tried when model switching fails (provider unavailable, rate limited, etc.).
106
106
 
107
+ - `skill_staleness_days`: number — skills unused for this many days get deprioritized during discovery. Set to `0` to disable staleness tracking. Default: `60`.
108
+
107
109
  - `skill_discovery`: controls how GSD discovers and applies skills during auto-mode. Valid values:
108
110
  - `auto` — skills are found and applied automatically without prompting.
109
111
  - `suggest` — (default) skills are identified during research but not installed automatically.
@@ -126,6 +128,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
126
128
  - `merge_strategy`: `"squash"` or `"merge"` — controls how worktree branches are merged back. `"squash"` combines all commits into one; `"merge"` preserves individual commits. Default: `"squash"`.
127
129
  - `isolation`: `"worktree"`, `"branch"`, or `"none"` — controls auto-mode git isolation strategy. `"worktree"` creates a milestone worktree for isolated work; `"branch"` works directly in the project root but creates a milestone branch (useful for submodule-heavy repos); `"none"` works directly on the current branch with no worktree or milestone branch (ideal for step-mode with hot reloads). Default: `"worktree"`.
128
130
  - `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`.
131
+ - `manage_gitignore`: boolean — when `false`, GSD will not touch `.gitignore` at all. Useful when your project has a strictly managed `.gitignore` and you don't want GSD adding entries. Default: `true`.
129
132
  - `worktree_post_create`: string — script to run after a worktree is created (both auto-mode and manual `/worktree`). Receives `SOURCE_DIR` and `WORKTREE_DIR` as environment variables. Can be absolute or relative to project root. Runs with 30-second timeout. Failure is non-fatal (logged as warning). Default: none.
130
133
 
131
134
  - `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`.
@@ -161,6 +164,31 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
161
164
  - `on_milestone`: boolean — notify when a milestone finishes. Default: `true`.
162
165
  - `on_attention`: boolean — notify when manual attention is needed. Default: `true`.
163
166
 
167
+ - `dynamic_routing`: configures the dynamic model router that adjusts model selection based on task complexity. Keys:
168
+ - `enabled`: boolean — enable dynamic routing. Default: `false`.
169
+ - `tier_models`: object — model overrides per complexity tier. Keys: `light`, `standard`, `heavy`. Values are model ID strings.
170
+ - `escalate_on_failure`: boolean — escalate to a higher-tier model when the current one fails. Default: `true`.
171
+ - `budget_pressure`: boolean — downgrade model tier when budget is under pressure. Default: `true`.
172
+ - `cross_provider`: boolean — allow routing across different providers. Default: `true`.
173
+ - `hooks`: boolean — enable routing hooks. Default: `true`.
174
+
175
+ - `auto_visualize`: boolean — show a visualizer hint after each milestone completion in auto-mode. Default: `false`.
176
+
177
+ - `auto_report`: boolean — generate an HTML report snapshot after each milestone completion. Default: `true`.
178
+
179
+ - `parallel`: configures parallel orchestration for running multiple slices concurrently. Keys:
180
+ - `enabled`: boolean — enable parallel execution. Default: `false`.
181
+ - `max_workers`: number — maximum concurrent workers (1-4). Default: `2`.
182
+ - `budget_ceiling`: number — optional per-parallel-run budget ceiling.
183
+ - `merge_strategy`: `"per-slice"` or `"per-milestone"` — when to merge worktree results back. Default: `"per-milestone"`.
184
+ - `auto_merge`: `"auto"`, `"confirm"`, or `"manual"` — merge behavior after completion. `"auto"` merges immediately; `"confirm"` asks first; `"manual"` leaves branches for you. Default: `"confirm"`.
185
+
186
+ - `verification_commands`: string[] — shell commands to run as verification after task execution (e.g., `["npm test", "npm run lint"]`). Commands run in order; if any fails, the task is marked as needing fixes.
187
+
188
+ - `verification_auto_fix`: boolean — when `true`, automatically attempt to fix verification failures instead of just reporting them. Default: `false`.
189
+
190
+ - `verification_max_retries`: number — maximum number of fix-and-retry cycles for verification failures. Default: `0` (no retries).
191
+
164
192
  - `uat_dispatch`: boolean — when `true`, enables UAT (User Acceptance Testing) dispatch mode. Default: `false`.
165
193
 
166
194
  - `post_unit_hooks`: array — hooks that fire after a unit completes. Each entry has:
@@ -531,3 +559,58 @@ remote_questions:
531
559
  ```
532
560
 
533
561
  Routes interactive questions to a Slack channel for headless auto-mode sessions. Questions time out after 15 minutes if unanswered.
562
+
563
+ ---
564
+
565
+ ## Dynamic Routing Example
566
+
567
+ ```yaml
568
+ ---
569
+ version: 1
570
+ dynamic_routing:
571
+ enabled: true
572
+ tier_models:
573
+ light: openrouter/minimax/minimax-m2.5
574
+ standard: claude-sonnet-4-6
575
+ heavy: claude-opus-4-6
576
+ escalate_on_failure: true
577
+ budget_pressure: true
578
+ ---
579
+ ```
580
+
581
+ Automatically selects model tier based on task complexity. Simple tasks use the `light` model, complex tasks escalate to `heavy`. Under budget pressure, tasks are routed to cheaper tiers.
582
+
583
+ ---
584
+
585
+ ## Parallel Execution Example
586
+
587
+ ```yaml
588
+ ---
589
+ version: 1
590
+ parallel:
591
+ enabled: true
592
+ max_workers: 3
593
+ merge_strategy: per-milestone
594
+ auto_merge: confirm
595
+ ---
596
+ ```
597
+
598
+ Runs up to 3 slices concurrently in separate worktrees. Results are merged per-milestone with user confirmation.
599
+
600
+ ---
601
+
602
+ ## Verification Example
603
+
604
+ ```yaml
605
+ ---
606
+ version: 1
607
+ verification_commands:
608
+ - npm test
609
+ - npm run lint
610
+ - npm run typecheck
611
+ verification_auto_fix: true
612
+ verification_max_retries: 2
613
+ ---
614
+ ```
615
+
616
+ Runs test, lint, and typecheck after each task. On failure, auto-fix is attempted up to 2 times before reporting the issue.
@@ -98,43 +98,106 @@ export function writeExportFile(
98
98
  export async function handleExport(args: string, ctx: ExtensionCommandContext, basePath: string): Promise<void> {
99
99
  // HTML report — delegates to the full visualizer-data pipeline
100
100
  if (args.includes("--html")) {
101
+ const generateAll = args.includes("--all");
101
102
  try {
102
103
  const { loadVisualizerData } = await import("./visualizer-data.js");
103
104
  const { generateHtmlReport } = await import("./export-html.js");
104
- const { writeReportSnapshot, reportsDir } = await import("./reports.js");
105
+ const { writeReportSnapshot, loadReportsIndex } = await import("./reports.js");
105
106
  const { basename: bn } = await import("node:path");
106
107
  const data = await loadVisualizerData(basePath);
107
108
  const projName = basename(basePath);
108
109
  const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
109
- const doneSlices = data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
110
- const totalSlices = data.milestones.reduce((s, m) => s + m.slices.length, 0);
111
- const outPath = writeReportSnapshot({
112
- basePath,
113
- html: generateHtmlReport(data, {
114
- projectName: projName,
115
- projectPath: basePath,
116
- gsdVersion,
117
- indexRelPath: "index.html",
118
- }),
119
- milestoneId: data.milestones.find(m => m.status === "active")?.id ?? "manual",
120
- milestoneTitle: data.milestones.find(m => m.status === "active")?.title ?? "",
121
- kind: "manual",
110
+ const doneMilestones = data.milestones.filter(m => m.status === "complete").length;
111
+
112
+ const htmlOpts = {
122
113
  projectName: projName,
123
114
  projectPath: basePath,
124
115
  gsdVersion,
125
- totalCost: data.totals?.cost ?? 0,
126
- totalTokens: data.totals?.tokens.total ?? 0,
127
- totalDuration: data.totals?.duration ?? 0,
128
- doneSlices,
129
- totalSlices,
130
- doneMilestones: data.milestones.filter(m => m.status === "complete").length,
131
- totalMilestones: data.milestones.length,
132
- phase: data.phase,
133
- });
134
- ctx.ui.notify(
135
- `HTML report saved: .gsd/reports/${bn(outPath)}\nBrowse all reports: .gsd/reports/index.html`,
136
- "success",
137
- );
116
+ indexRelPath: "index.html",
117
+ };
118
+
119
+ if (generateAll) {
120
+ // Generate a report snapshot for every milestone
121
+ const existing = loadReportsIndex(basePath);
122
+ const existingIds = new Set(existing?.entries.map(e => e.milestoneId) ?? []);
123
+
124
+ const targets = data.milestones.filter(m => !existingIds.has(m.id));
125
+ if (targets.length === 0) {
126
+ ctx.ui.notify(
127
+ "All milestones already have report snapshots. Run without --all to create a new snapshot for the active milestone.",
128
+ "info",
129
+ );
130
+ return;
131
+ }
132
+
133
+ const html = generateHtmlReport(data, htmlOpts);
134
+ const paths: string[] = [];
135
+
136
+ for (const ms of targets) {
137
+ const msSlicesDone = ms.slices.filter(sl => sl.done).length;
138
+ const msSlicesTotal = ms.slices.length;
139
+
140
+ // Accumulate project-wide progress up to and including this milestone
141
+ const msIdx = data.milestones.indexOf(ms);
142
+ let cumulativeDone = 0;
143
+ let cumulativeTotal = 0;
144
+ for (let i = 0; i <= msIdx; i++) {
145
+ cumulativeDone += data.milestones[i].slices.filter(sl => sl.done).length;
146
+ cumulativeTotal += data.milestones[i].slices.length;
147
+ }
148
+
149
+ const outPath = writeReportSnapshot({
150
+ basePath,
151
+ html,
152
+ milestoneId: ms.id,
153
+ milestoneTitle: ms.title,
154
+ kind: ms.status === "complete" ? "milestone" : "manual",
155
+ projectName: projName,
156
+ projectPath: basePath,
157
+ gsdVersion,
158
+ totalCost: data.totals?.cost ?? 0,
159
+ totalTokens: data.totals?.tokens.total ?? 0,
160
+ totalDuration: data.totals?.duration ?? 0,
161
+ doneSlices: cumulativeDone,
162
+ totalSlices: cumulativeTotal,
163
+ doneMilestones: data.milestones.slice(0, msIdx + 1).filter(m => m.status === "complete").length,
164
+ totalMilestones: data.milestones.length,
165
+ phase: ms.status === "complete" ? "complete" : data.phase,
166
+ });
167
+ paths.push(bn(outPath));
168
+ }
169
+
170
+ ctx.ui.notify(
171
+ `Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\nBrowse all reports: .gsd/reports/index.html`,
172
+ "success",
173
+ );
174
+ } else {
175
+ // Single report for the active milestone (existing behavior)
176
+ const doneSlices = data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
177
+ const totalSlices = data.milestones.reduce((s, m) => s + m.slices.length, 0);
178
+ const outPath = writeReportSnapshot({
179
+ basePath,
180
+ html: generateHtmlReport(data, htmlOpts),
181
+ milestoneId: data.milestones.find(m => m.status === "active")?.id ?? "manual",
182
+ milestoneTitle: data.milestones.find(m => m.status === "active")?.title ?? "",
183
+ kind: "manual",
184
+ projectName: projName,
185
+ projectPath: basePath,
186
+ gsdVersion,
187
+ totalCost: data.totals?.cost ?? 0,
188
+ totalTokens: data.totals?.tokens.total ?? 0,
189
+ totalDuration: data.totals?.duration ?? 0,
190
+ doneSlices,
191
+ totalSlices,
192
+ doneMilestones,
193
+ totalMilestones: data.milestones.length,
194
+ phase: data.phase,
195
+ });
196
+ ctx.ui.notify(
197
+ `HTML report saved: .gsd/reports/${bn(outPath)}\nBrowse all reports: .gsd/reports/index.html`,
198
+ "success",
199
+ );
200
+ }
138
201
  } catch (err) {
139
202
  ctx.ui.notify(
140
203
  `HTML export failed: ${err instanceof Error ? err.message : String(err)}`,
@@ -25,7 +25,7 @@ A researcher explored the codebase and a planner decomposed the work — you are
25
25
  {{priorTaskLines}}
26
26
 
27
27
  Then:
28
- 0. Narrate step transitions, key implementation decisions, and verification outcomes as you work. Keep it terse — one line between tool-call clusters, not between every call.
28
+ 0. Narrate step transitions, key implementation decisions, and verification outcomes as you work. Keep it terse — one line between tool-call clusters, not between every call — but write complete sentences in user-facing prose, not shorthand notes or scratchpad fragments.
29
29
  1. **Load relevant skills before writing code.** Check the `GSD Skill Preferences` block in system context and the `<available_skills>` catalog in your system prompt. For each skill that matches this task's technology stack (e.g., React, Next.js, accessibility, component design), `read` its SKILL.md file now. Skills contain implementation rules and patterns that should guide your code. If no skills match this task, skip this step.
30
30
  2. Execute the steps in the inlined task plan
31
31
  3. Build the real thing. If the task plan says "create login endpoint", build an endpoint that actually authenticates against a real store, not one that returns a hardcoded success response. If the task plan says "create dashboard page", build a page that renders real data from the API, not a component with hardcoded props. Stubs and mocks are for tests, not for the shipped feature.
@@ -16,7 +16,7 @@ A **researcher agent** already explored the codebase and documented findings in
16
16
 
17
17
  After you finish, each slice goes through its own research → plan → execute cycle. Slice researchers dive deeper into the specific area. Slice planners decompose into tasks. Executors build each task. Your roadmap sets the strategic frame for all of them.
18
18
 
19
- Narrate your decomposition reasoning — why you're grouping work this way, what risks are driving the order, what verification strategy you're choosing and why.
19
+ Narrate your decomposition reasoning — why you're grouping work this way, what risks are driving the order, what verification strategy you're choosing and why. Use complete sentences rather than planner shorthand or fragmentary notes.
20
20
 
21
21
  Then:
22
22
  1. Use the **Roadmap** output template from the inlined context above