gsd-pi 2.37.0 → 2.37.1-dev.49503be

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 (93) hide show
  1. package/README.md +21 -20
  2. package/dist/onboarding.js +1 -0
  3. package/dist/resources/extensions/cmux/package.json +7 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +54 -1
  5. package/dist/resources/extensions/gsd/auto-loop.js +18 -4
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
  7. package/dist/resources/extensions/gsd/auto-prompts.js +55 -0
  8. package/dist/resources/extensions/gsd/auto-recovery.js +19 -1
  9. package/dist/resources/extensions/gsd/auto.js +42 -5
  10. package/dist/resources/extensions/gsd/commands.js +80 -33
  11. package/dist/resources/extensions/gsd/files.js +41 -0
  12. package/dist/resources/extensions/gsd/git-service.js +9 -1
  13. package/dist/resources/extensions/gsd/history.js +2 -1
  14. package/dist/resources/extensions/gsd/metrics.js +4 -2
  15. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  16. package/dist/resources/extensions/gsd/preferences-validation.js +42 -0
  17. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  18. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  19. package/dist/resources/extensions/gsd/session-lock.js +26 -6
  20. package/dist/resources/extensions/shared/format-utils.js +5 -41
  21. package/dist/resources/extensions/shared/layout-utils.js +46 -0
  22. package/dist/resources/extensions/shared/mod.js +2 -1
  23. package/package.json +2 -1
  24. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  25. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  26. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  27. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  28. package/packages/pi-ai/dist/models.generated.js +172 -0
  29. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  30. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  31. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  32. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  33. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  34. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  35. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  36. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  37. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  38. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  39. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  40. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  41. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  42. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  43. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  44. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  45. package/packages/pi-ai/dist/types.d.ts +2 -2
  46. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  47. package/packages/pi-ai/dist/types.js.map +1 -1
  48. package/packages/pi-ai/package.json +1 -0
  49. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  50. package/packages/pi-ai/src/models.generated.ts +172 -0
  51. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  52. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  53. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  54. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  55. package/packages/pi-ai/src/types.ts +2 -0
  56. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  57. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -4
  58. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  61. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  62. package/packages/pi-coding-agent/package.json +1 -1
  63. package/packages/pi-coding-agent/src/core/extensions/loader.ts +8 -4
  64. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  65. package/pkg/package.json +1 -1
  66. package/src/resources/extensions/cmux/package.json +7 -0
  67. package/src/resources/extensions/gsd/auto-dispatch.ts +78 -0
  68. package/src/resources/extensions/gsd/auto-loop.ts +24 -6
  69. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
  70. package/src/resources/extensions/gsd/auto-prompts.ts +68 -0
  71. package/src/resources/extensions/gsd/auto-recovery.ts +18 -0
  72. package/src/resources/extensions/gsd/auto.ts +56 -5
  73. package/src/resources/extensions/gsd/commands.ts +85 -31
  74. package/src/resources/extensions/gsd/files.ts +45 -0
  75. package/src/resources/extensions/gsd/git-service.ts +12 -1
  76. package/src/resources/extensions/gsd/history.ts +2 -1
  77. package/src/resources/extensions/gsd/metrics.ts +4 -2
  78. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  79. package/src/resources/extensions/gsd/preferences-validation.ts +41 -0
  80. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  81. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  82. package/src/resources/extensions/gsd/session-lock.ts +41 -6
  83. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +37 -1
  84. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +19 -0
  85. package/src/resources/extensions/gsd/tests/cmux.test.ts +25 -1
  86. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +367 -0
  87. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  88. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +45 -0
  89. package/src/resources/extensions/gsd/types.ts +41 -0
  90. package/src/resources/extensions/shared/format-utils.ts +5 -44
  91. package/src/resources/extensions/shared/layout-utils.ts +49 -0
  92. package/src/resources/extensions/shared/mod.ts +7 -4
  93. package/src/resources/extensions/shared/tests/format-utils.test.ts +5 -3
package/README.md CHANGED
@@ -16,7 +16,7 @@ This version is different. GSD is now a standalone CLI built on the [Pi SDK](htt
16
16
 
17
17
  One command. Walk away. Come back to a built project with clean git history.
18
18
 
19
- <pre><code>npm install -g gsd-pi</code></pre>
19
+ <pre><code>npm install -g gsd-pi@latest</code></pre>
20
20
 
21
21
  > **📋 NOTICE: New to Node on Mac?** If you installed Node.js via Homebrew, you may be running a development release instead of LTS. **[Read this guide](./docs/node-lts-macos.md)** to pin Node 24 LTS and avoid compatibility issues.
22
22
 
@@ -24,19 +24,19 @@ One command. Walk away. Come back to a built project with clean git history.
24
24
 
25
25
  ---
26
26
 
27
- ## What's New in v2.33
27
+ ## What's New in v2.37
28
28
 
29
- - **Dispatch loop hardening** — defensive guards, reentrancy protection, and 125 new regression tests covering the full `deriveState → resolveDispatch` chain without an LLM
30
- - **Live regression test harness** — post-build pipeline validation that catches dispatch, parser, and lock lifecycle regressions before promotion
31
- - **Unified error handling** — `getErrorMessage()` helper replaces 65 inline duplicates across the codebase
32
- - **Centralized unit ID parsing** — `parseUnitId()` eliminates fragile regex patterns scattered across dispatch, recovery, and metrics code
33
- - **Milestone merge consolidation** — `tryMergeMilestone()` replaces 4 duplicate merge paths in the auto-mode loop
34
- - **Lock alignment fix** — retry lock path now matches primary lock settings, preventing `ECOMPROMISED` errors on resume
35
- - **NixOS/nix-darwin support** — symlinks in `.gsd/` are skipped during `makeTreeWritable` to prevent `EPERM` failures
36
- - **Windows EPERM fallback** — `.gsd/` migration uses copy+delete when NTFS blocks direct rename
37
- - **Worktree identity fix** — stable project hash resolved from main repo root, not worktree path
38
- - **Quick-task branch cleanup** — `/gsd quick` branches auto-merge back to the original branch after completion
39
- - **Crash recovery guidance** actionable next-step messages based on what was interrupted and what state survived
29
+ - **cmux integration** — sidebar status, progress bars, and notifications for [cmux](https://cmux.com) terminal multiplexer users
30
+ - **Redesigned dashboard** — two-column layout with redesigned widget
31
+ - **Search budget enforcement** — session-level search budget prevents unbounded native web search
32
+ - **AGENTS.md support** — deprecated `agent-instructions.md` in favor of standard `AGENTS.md` / `CLAUDE.md`
33
+ - **AI-powered triage** — automated issue and PR triage via Claude Haiku
34
+ - **Auto-generated OpenRouter registry** — model registry built from OpenRouter API for always-current model support
35
+ - **Extension manifest system** — user-managed enable/disable for bundled extensions
36
+ - **Pipeline simplification (ADR-003)** — merged research into planning, mechanical completion
37
+ - **Workflow templates** — right-sized workflows for every task type
38
+ - **Health widget** — always-on environment health checks with progress scoring
39
+ - **`/gsd changelog`**LLM-summarized release notes for any version
40
40
 
41
41
  See the full [Changelog](./CHANGELOG.md) for details.
42
42
 
@@ -49,7 +49,7 @@ Full documentation is available in the [`docs/`](./docs/) directory:
49
49
  - **[Getting Started](./docs/getting-started.md)** — install, first run, basic usage
50
50
  - **[Auto Mode](./docs/auto-mode.md)** — autonomous execution deep-dive
51
51
  - **[Configuration](./docs/configuration.md)** — all preferences, models, git, and hooks
52
- - **[Token Optimization](./docs/token-optimization.md)** — profiles, context compression, complexity routing (v2.17)
52
+ - **[Token Optimization](./docs/token-optimization.md)** — profiles, context compression, complexity routing
53
53
  - **[Cost Management](./docs/cost-management.md)** — budgets, tracking, projections
54
54
  - **[Git Strategy](./docs/git-strategy.md)** — worktree isolation, branching, merge behavior
55
55
  - **[Parallel Orchestration](./docs/parallel-orchestration.md)** — run multiple milestones simultaneously
@@ -463,9 +463,9 @@ Place an `AGENTS.md` file in any directory to provide persistent behavioral guid
463
463
 
464
464
  Start GSD with `gsd --debug` to enable structured JSONL diagnostic logging. Debug logs capture dispatch decisions, state transitions, and timing data for troubleshooting auto-mode issues.
465
465
 
466
- ### Token Optimization (v2.17)
466
+ ### Token Optimization
467
467
 
468
- GSD 2.17 introduced a coordinated token optimization system that reduces usage by 40-60% on cost-sensitive workloads. Set a single preference to coordinate model selection, phase skipping, and context compression:
468
+ GSD includes a coordinated token optimization system that reduces usage by 40-60% on cost-sensitive workloads. Set a single preference to coordinate model selection, phase skipping, and context compression:
469
469
 
470
470
  ```yaml
471
471
  token_profile: budget # or balanced (default), quality
@@ -485,7 +485,7 @@ See the full [Token Optimization Guide](./docs/token-optimization.md) for detail
485
485
 
486
486
  ### Bundled Tools
487
487
 
488
- GSD ships with 18 extensions, all loaded automatically:
488
+ GSD ships with 19 extensions, all loaded automatically:
489
489
 
490
490
  | Extension | What it provides |
491
491
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
@@ -495,12 +495,13 @@ GSD ships with 18 extensions, all loaded automatically:
495
495
  | **Google Search** | Gemini-powered web search with AI-synthesized answers |
496
496
  | **Context7** | Up-to-date library/framework documentation |
497
497
  | **Background Shell** | Long-running process management with readiness detection |
498
+ | **Async Jobs** | Background bash commands with job tracking and cancellation |
498
499
  | **Subagent** | Delegated tasks with isolated context windows |
500
+ | **GitHub** | Full-suite GitHub issues and PR management via `/gh` command |
499
501
  | **Mac Tools** | macOS native app automation via Accessibility APIs |
500
502
  | **MCP Client** | Native MCP server integration via @modelcontextprotocol/sdk |
501
503
  | **Voice** | Real-time speech-to-text transcription (macOS, Linux — Ubuntu 22.04+) |
502
504
  | **Slash Commands** | Custom command creation |
503
- | **LSP** | Language Server Protocol integration — diagnostics, go-to-definition, references, hover, symbols, rename, code actions |
504
505
  | **Ask User Questions** | Structured user input with single/multi-select |
505
506
  | **Secure Env Collect** | Masked secret collection without manual .env editing |
506
507
  | **Remote Questions** | Route decisions to Slack/Discord when human input is needed in headless/CI mode |
@@ -591,7 +592,7 @@ gsd (CLI binary)
591
592
  ├─ resource-loader.ts Syncs bundled extensions + agents to ~/.gsd/agent/
592
593
  └─ src/resources/
593
594
  ├─ extensions/gsd/ Core GSD extension (auto, state, commands, ...)
594
- ├─ extensions/... 12 supporting extensions
595
+ ├─ extensions/... 18 supporting extensions
595
596
  ├─ agents/ scout, researcher, worker
596
597
  ├─ AGENTS.md Agent routing instructions
597
598
  └─ GSD-WORKFLOW.md Manual bootstrap protocol
@@ -628,7 +629,7 @@ GSD isn't locked to one provider. It runs on the [Pi SDK](https://github.com/bad
628
629
 
629
630
  ### Built-in Providers
630
631
 
631
- Anthropic, OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock, Azure OpenAI, Google Vertex, Groq, Cerebras, Mistral, xAI, HuggingFace, Vercel AI Gateway, and more.
632
+ Anthropic, Anthropic (Vertex AI), OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock, Azure OpenAI, Google Vertex, Groq, Cerebras, Mistral, xAI, HuggingFace, Vercel AI Gateway, and more.
632
633
 
633
634
  ### OAuth / Max Plans
634
635
 
@@ -38,6 +38,7 @@ const TOOL_KEYS = [
38
38
  /** Known LLM provider IDs that, if authed, mean the user doesn't need onboarding */
39
39
  const LLM_PROVIDER_IDS = [
40
40
  'anthropic',
41
+ 'anthropic-vertex',
41
42
  'openai',
42
43
  'github-copilot',
43
44
  'openai-codex',
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@gsd/cmux",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "cmux integration library — used by other extensions, not an extension itself",
6
+ "pi": {}
7
+ }
@@ -12,7 +12,7 @@ import { loadFile, loadActiveOverrides, parseRoadmap } from "./files.js";
12
12
  import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile, relSliceFile, buildMilestoneFileName, } from "./paths.js";
13
13
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
14
14
  import { join } from "node:path";
15
- import { buildResearchMilestonePrompt, buildPlanMilestonePrompt, buildResearchSlicePrompt, buildPlanSlicePrompt, buildExecuteTaskPrompt, buildCompleteSlicePrompt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt, buildReplanSlicePrompt, buildRunUatPrompt, buildReassessRoadmapPrompt, buildRewriteDocsPrompt, checkNeedsReassessment, checkNeedsRunUat, } from "./auto-prompts.js";
15
+ import { buildResearchMilestonePrompt, buildPlanMilestonePrompt, buildResearchSlicePrompt, buildPlanSlicePrompt, buildExecuteTaskPrompt, buildCompleteSlicePrompt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt, buildReplanSlicePrompt, buildRunUatPrompt, buildReassessRoadmapPrompt, buildRewriteDocsPrompt, buildReactiveExecutePrompt, checkNeedsReassessment, checkNeedsRunUat, } from "./auto-prompts.js";
16
16
  function missingSliceStop(mid, phase) {
17
17
  return {
18
18
  action: "stop",
@@ -223,6 +223,59 @@ const DISPATCH_RULES = [
223
223
  };
224
224
  },
225
225
  },
226
+ {
227
+ name: "executing → reactive-execute (parallel dispatch)",
228
+ match: async ({ state, mid, midTitle, basePath, prefs }) => {
229
+ if (state.phase !== "executing" || !state.activeTask)
230
+ return null;
231
+ if (!state.activeSlice)
232
+ return null; // fall through
233
+ // Only activate when reactive_execution is explicitly enabled
234
+ const reactiveConfig = prefs?.reactive_execution;
235
+ if (!reactiveConfig?.enabled)
236
+ return null;
237
+ const sid = state.activeSlice.id;
238
+ const sTitle = state.activeSlice.title;
239
+ const maxParallel = reactiveConfig.max_parallel ?? 2;
240
+ // Dry-run mode: max_parallel=1 means graph is derived and logged but
241
+ // execution remains sequential
242
+ if (maxParallel <= 1)
243
+ return null;
244
+ try {
245
+ const { loadSliceTaskIO, deriveTaskGraph, isGraphAmbiguous, getReadyTasks, chooseNonConflictingSubset, graphMetrics, } = await import("./reactive-graph.js");
246
+ const taskIO = await loadSliceTaskIO(basePath, mid, sid);
247
+ if (taskIO.length < 2)
248
+ return null; // single task, no point
249
+ const graph = deriveTaskGraph(taskIO);
250
+ // Ambiguous graph → fall through to sequential
251
+ if (isGraphAmbiguous(graph))
252
+ return null;
253
+ const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
254
+ const readyIds = getReadyTasks(graph, completed, new Set());
255
+ // Only activate reactive dispatch when >1 task is ready
256
+ if (readyIds.length <= 1)
257
+ return null;
258
+ const selected = chooseNonConflictingSubset(readyIds, graph, maxParallel, new Set());
259
+ if (selected.length <= 1)
260
+ return null;
261
+ // Log graph metrics for observability
262
+ const metrics = graphMetrics(graph);
263
+ process.stderr.write(`gsd-reactive: ${mid}/${sid} graph — tasks:${metrics.taskCount} edges:${metrics.edgeCount} ` +
264
+ `ready:${metrics.readySetSize} dispatching:${selected.length} ambiguous:${metrics.ambiguous}\n`);
265
+ return {
266
+ action: "dispatch",
267
+ unitType: "reactive-execute",
268
+ unitId: `${mid}/${sid}/reactive`,
269
+ prompt: await buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, selected, basePath),
270
+ };
271
+ }
272
+ catch (err) {
273
+ // Non-fatal — fall through to sequential execution
274
+ process.stderr.write(`gsd-reactive: graph derivation failed: ${err.message}\n`);
275
+ return null;
276
+ }
277
+ },
278
+ },
226
279
  {
227
280
  name: "executing → execute-task (recover missing task plan → plan-slice)",
228
281
  match: async ({ state, mid, midTitle, basePath }) => {
@@ -218,10 +218,24 @@ export async function autoLoop(ctx, pi, s, deps) {
218
218
  }
219
219
  try {
220
220
  // ── Blanket try/catch: one bad iteration must not kill the session
221
- if (deps.lockBase() && !deps.validateSessionLock(deps.lockBase())) {
222
- deps.handleLostSessionLock(ctx);
223
- debugLog("autoLoop", { phase: "exit", reason: "session-lock-lost" });
224
- break;
221
+ const sessionLockBase = deps.lockBase();
222
+ if (sessionLockBase) {
223
+ const lockStatus = deps.validateSessionLock(sessionLockBase);
224
+ if (!lockStatus.valid) {
225
+ debugLog("autoLoop", {
226
+ phase: "session-lock-invalid",
227
+ reason: lockStatus.failureReason ?? "unknown",
228
+ existingPid: lockStatus.existingPid,
229
+ expectedPid: lockStatus.expectedPid,
230
+ });
231
+ deps.handleLostSessionLock(ctx, lockStatus);
232
+ debugLog("autoLoop", {
233
+ phase: "exit",
234
+ reason: "session-lock-lost",
235
+ detail: lockStatus.failureReason ?? "unknown",
236
+ });
237
+ break;
238
+ }
225
239
  }
226
240
  // ── Phase 1: Pre-dispatch ───────────────────────────────────────────
227
241
  // Resource version guard
@@ -171,6 +171,20 @@ export async function postUnitPreVerification(pctx) {
171
171
  // Non-fatal
172
172
  }
173
173
  }
174
+ // Reactive state cleanup on slice completion
175
+ if (s.currentUnit.type === "complete-slice") {
176
+ try {
177
+ const parts = s.currentUnit.id.split("/");
178
+ const [mid, sid] = parts;
179
+ if (mid && sid) {
180
+ const { clearReactiveState } = await import("./reactive-graph.js");
181
+ clearReactiveState(s.basePath, mid, sid);
182
+ }
183
+ }
184
+ catch {
185
+ // Non-fatal
186
+ }
187
+ }
174
188
  // Post-triage: execute actionable resolutions
175
189
  if (s.currentUnit.type === "triage-captures") {
176
190
  try {
@@ -1090,6 +1090,61 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
1090
1090
  commitInstruction: reassessCommitInstruction,
1091
1091
  });
1092
1092
  }
1093
+ // ─── Reactive Execute Prompt ──────────────────────────────────────────────
1094
+ export async function buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, readyTaskIds, base) {
1095
+ const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
1096
+ // Build graph for context
1097
+ const taskIO = await loadSliceTaskIO(base, mid, sid);
1098
+ const graph = deriveTaskGraph(taskIO);
1099
+ const metrics = graphMetrics(graph);
1100
+ // Build graph context section
1101
+ const graphLines = [];
1102
+ for (const node of graph) {
1103
+ const status = node.done ? "✅ done" : readyTaskIds.includes(node.id) ? "🟢 ready" : "⏳ waiting";
1104
+ const deps = node.dependsOn.length > 0 ? ` (depends on: ${node.dependsOn.join(", ")})` : "";
1105
+ graphLines.push(`- **${node.id}: ${node.title}** — ${status}${deps}`);
1106
+ if (node.outputFiles.length > 0) {
1107
+ graphLines.push(` - Outputs: ${node.outputFiles.map(f => `\`${f}\``).join(", ")}`);
1108
+ }
1109
+ }
1110
+ const graphContext = [
1111
+ `Tasks: ${metrics.taskCount}, Edges: ${metrics.edgeCount}, Ready: ${metrics.readySetSize}`,
1112
+ "",
1113
+ ...graphLines,
1114
+ ].join("\n");
1115
+ // Build individual subagent prompts for each ready task
1116
+ const subagentSections = [];
1117
+ const readyTaskListLines = [];
1118
+ for (const tid of readyTaskIds) {
1119
+ const node = graph.find((n) => n.id === tid);
1120
+ const tTitle = node?.title ?? tid;
1121
+ readyTaskListLines.push(`- **${tid}: ${tTitle}**`);
1122
+ // Build a full execute-task prompt for this task (reuse existing builder)
1123
+ const taskPrompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base);
1124
+ subagentSections.push([
1125
+ `### ${tid}: ${tTitle}`,
1126
+ "",
1127
+ "Use this as the prompt for a `subagent` call:",
1128
+ "",
1129
+ "```",
1130
+ taskPrompt,
1131
+ "```",
1132
+ ].join("\n"));
1133
+ }
1134
+ const inlinedTemplates = inlineTemplate("task-summary", "Task Summary");
1135
+ return loadPrompt("reactive-execute", {
1136
+ workingDirectory: base,
1137
+ milestoneId: mid,
1138
+ milestoneTitle: midTitle,
1139
+ sliceId: sid,
1140
+ sliceTitle: sTitle,
1141
+ graphContext,
1142
+ readyTaskCount: String(readyTaskIds.length),
1143
+ readyTaskList: readyTaskListLines.join("\n"),
1144
+ subagentPrompts: subagentSections.join("\n\n---\n\n"),
1145
+ inlinedTemplates,
1146
+ });
1147
+ }
1093
1148
  export async function buildRewriteDocsPrompt(mid, midTitle, activeSlice, base, overrides) {
1094
1149
  const sid = activeSlice?.id;
1095
1150
  const sTitle = activeSlice?.title ?? "";
@@ -11,7 +11,7 @@ import { clearUnitRuntimeRecord } from "./unit-runtime.js";
11
11
  import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
12
12
  import { isValidationTerminal } from "./state.js";
13
13
  import { nativeConflictFiles, nativeCommit, nativeCheckoutTheirs, nativeAddPaths, nativeMergeAbort, nativeResetHard, } from "./native-git-bridge.js";
14
- import { resolveMilestonePath, resolveSlicePath, resolveSliceFile, resolveTasksDir, relMilestoneFile, relSliceFile, relSlicePath, relTaskFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
14
+ import { resolveMilestonePath, resolveSlicePath, resolveSliceFile, resolveTasksDir, resolveTaskFiles, relMilestoneFile, relSliceFile, relSlicePath, relTaskFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
15
15
  import { markSliceDoneInRoadmap } from "./roadmap-mutations.js";
16
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, } from "node:fs";
17
17
  import { dirname, join } from "node:path";
@@ -73,6 +73,9 @@ export function resolveExpectedArtifactPath(unitType, unitId, base) {
73
73
  }
74
74
  case "rewrite-docs":
75
75
  return null;
76
+ case "reactive-execute":
77
+ // Reactive execute produces multiple task summaries — verified separately
78
+ return null;
76
79
  default:
77
80
  return null;
78
81
  }
@@ -105,6 +108,21 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
105
108
  const content = readFileSync(overridesPath, "utf-8");
106
109
  return !content.includes("**Scope:** active");
107
110
  }
111
+ // Reactive-execute: verify that at least one new task summary was written.
112
+ // The unitId is "{mid}/{sid}/reactive" — extract mid and sid to check.
113
+ if (unitType === "reactive-execute") {
114
+ const parts = unitId.split("/");
115
+ const mid = parts[0];
116
+ const sid = parts[1];
117
+ if (!mid || !sid)
118
+ return false;
119
+ const tDir = resolveTasksDir(base, mid, sid);
120
+ if (!tDir)
121
+ return false;
122
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
123
+ // At least one summary file should exist
124
+ return summaryFiles.length > 0;
125
+ }
108
126
  const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
109
127
  // For unit types with no verifiable artifact (null path), the parent directory
110
128
  // is missing on disk — treat as stale completion state so the key gets evicted (#313).
@@ -18,7 +18,7 @@ import { invalidateAllCaches } from "./cache.js";
18
18
  import { clearActivityLogState } from "./activity-log.js";
19
19
  import { synthesizeCrashRecovery, getDeepDiagnostic, } from "./session-forensics.js";
20
20
  import { writeLock, clearLock, readCrashLock, isLockProcessAlive, } from "./crash-recovery.js";
21
- import { acquireSessionLock, validateSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
21
+ import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
22
22
  import { clearUnitRuntimeRecord, readUnitRuntimeRecord, writeUnitRuntimeRecord, } from "./unit-runtime.js";
23
23
  import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, } from "./preferences.js";
24
24
  import { sendDesktopNotification } from "./notifications.js";
@@ -209,6 +209,29 @@ export function stopAutoRemote(projectRoot) {
209
209
  return { found: false, error: err.message };
210
210
  }
211
211
  }
212
+ /**
213
+ * Check if a remote auto-mode session is running (from a different process).
214
+ * Reads the crash lock, checks PID liveness, and returns session details.
215
+ * Used by the guard in commands.ts to prevent bare /gsd, /gsd next, and
216
+ * /gsd auto from stealing the session lock.
217
+ */
218
+ export function checkRemoteAutoSession(projectRoot) {
219
+ const lock = readCrashLock(projectRoot);
220
+ if (!lock)
221
+ return { running: false };
222
+ if (!isLockProcessAlive(lock)) {
223
+ // Stale lock from a dead process — not a live remote session
224
+ return { running: false };
225
+ }
226
+ return {
227
+ running: true,
228
+ pid: lock.pid,
229
+ unitType: lock.unitType,
230
+ unitId: lock.unitId,
231
+ startedAt: lock.startedAt,
232
+ completedUnits: lock.completedUnits,
233
+ };
234
+ }
212
235
  export function isStepMode() {
213
236
  return s.stepMode;
214
237
  }
@@ -243,14 +266,28 @@ function buildSnapshotOpts(unitType, unitId) {
243
266
  ...(runtime?.continueHereFired ? { continueHereFired: true } : {}),
244
267
  };
245
268
  }
246
- function handleLostSessionLock(ctx) {
247
- debugLog("session-lock-lost", { lockBase: lockBase() });
269
+ function handleLostSessionLock(ctx, lockStatus) {
270
+ debugLog("session-lock-lost", {
271
+ lockBase: lockBase(),
272
+ reason: lockStatus?.failureReason,
273
+ existingPid: lockStatus?.existingPid,
274
+ expectedPid: lockStatus?.expectedPid,
275
+ });
248
276
  s.active = false;
249
277
  s.paused = false;
250
278
  clearUnitTimeout();
251
279
  deregisterSigtermHandler();
252
280
  clearCmuxSidebar(loadEffectiveGSDPreferences()?.preferences);
253
- ctx?.ui.notify("Session lock lost another GSD process appears to have taken over. Stopping gracefully.", "error");
281
+ const message = lockStatus?.failureReason === "pid-mismatch"
282
+ ? lockStatus.existingPid
283
+ ? `Session lock moved to PID ${lockStatus.existingPid} — another GSD process appears to have taken over. Stopping gracefully.`
284
+ : "Session lock moved to a different process — another GSD process appears to have taken over. Stopping gracefully."
285
+ : lockStatus?.failureReason === "missing-metadata"
286
+ ? "Session lock metadata disappeared, so ownership could not be confirmed. Stopping gracefully."
287
+ : lockStatus?.failureReason === "compromised"
288
+ ? "Session lock was compromised or invalidated during heartbeat checks; takeover was not confirmed. Stopping gracefully."
289
+ : "Session lock lost. Stopping gracefully.";
290
+ ctx?.ui.notify(message, "error");
254
291
  ctx?.ui.setStatus("gsd-auto", undefined);
255
292
  ctx?.ui.setWidget("gsd-progress", undefined);
256
293
  ctx?.ui.setFooter(undefined);
@@ -473,7 +510,7 @@ function buildLoopDeps() {
473
510
  // Resource version guard
474
511
  checkResourcesStale,
475
512
  // Session lock
476
- validateSessionLock,
513
+ validateSessionLock: getSessionLockStatus,
477
514
  updateSessionLock,
478
515
  handleLostSessionLock,
479
516
  // Milestone transition
@@ -12,7 +12,7 @@ import { deriveState } from "./state.js";
12
12
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
13
13
  import { GSDVisualizerOverlay } from "./visualizer-overlay.js";
14
14
  import { showQueue, showDiscuss, showHeadlessMilestoneCreation } from "./guided-flow.js";
15
- import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, stopAutoRemote } from "./auto.js";
15
+ import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, stopAutoRemote, checkRemoteAutoSession } from "./auto.js";
16
16
  import { dispatchDirectPhase } from "./auto-direct-dispatch.js";
17
17
  import { resolveProjectRoot } from "./worktree.js";
18
18
  import { assertSafeDirectory } from "./validate-directory.js";
@@ -36,8 +36,8 @@ import { computeProgressScore, formatProgressLine } from "./progress-score.js";
36
36
  import { runEnvironmentChecks } from "./doctor-environment.js";
37
37
  import { handleLogs } from "./commands-logs.js";
38
38
  import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
39
- import { readSessionLockData, isSessionLockProcessAlive } from "./session-lock.js";
40
39
  import { handleCmux } from "./commands-cmux.js";
40
+ import { showNextAction } from "../shared/mod.js";
41
41
  /** Resolve the effective project root, accounting for worktree paths. */
42
42
  export function projectRoot() {
43
43
  const cwd = process.cwd();
@@ -57,36 +57,81 @@ export function projectRoot() {
57
57
  return root;
58
58
  }
59
59
  /**
60
- * Check if another process holds the auto-mode session lock.
61
- * Returns the lock data if a remote session is alive, null otherwise.
60
+ * Guard against starting auto-mode when a remote session is already running.
61
+ * Returns true if the caller should proceed with startAuto, false if handled.
62
62
  */
63
- function getRemoteAutoSession(basePath) {
64
- const lockData = readSessionLockData(basePath);
65
- if (!lockData)
66
- return null;
67
- if (lockData.pid === process.pid)
68
- return null;
69
- if (!isSessionLockProcessAlive(lockData))
70
- return null;
71
- return { pid: lockData.pid };
72
- }
73
- /**
74
- * Show a steering menu when auto-mode is running in another process.
75
- * Returns true if a remote session was detected (caller should return early).
76
- */
77
- function notifyRemoteAutoActive(ctx, basePath) {
78
- const remote = getRemoteAutoSession(basePath);
79
- if (!remote)
63
+ async function guardRemoteSession(ctx, pi) {
64
+ // Local session already active — proceed (startAuto handles re-entrant calls)
65
+ if (isAutoActive() || isAutoPaused())
66
+ return true;
67
+ const remote = checkRemoteAutoSession(projectRoot());
68
+ if (!remote.running || !remote.pid)
69
+ return true;
70
+ const unitLabel = remote.unitType && remote.unitId
71
+ ? `${remote.unitType} (${remote.unitId})`
72
+ : "unknown unit";
73
+ const unitsMsg = remote.completedUnits != null
74
+ ? `${remote.completedUnits} units completed`
75
+ : "";
76
+ const choice = await showNextAction(ctx, {
77
+ title: `Auto-mode is running in another terminal (PID ${remote.pid})`,
78
+ summary: [
79
+ `Currently executing: ${unitLabel}`,
80
+ ...(unitsMsg ? [unitsMsg] : []),
81
+ ...(remote.startedAt ? [`Started: ${remote.startedAt}`] : []),
82
+ ],
83
+ actions: [
84
+ {
85
+ id: "status",
86
+ label: "View status",
87
+ description: "Show the current GSD progress dashboard.",
88
+ recommended: true,
89
+ },
90
+ {
91
+ id: "steer",
92
+ label: "Steer the session",
93
+ description: "Use /gsd steer <instruction> to redirect the running session.",
94
+ },
95
+ {
96
+ id: "stop",
97
+ label: "Stop remote session",
98
+ description: `Send SIGTERM to PID ${remote.pid} to stop it gracefully.`,
99
+ },
100
+ {
101
+ id: "force",
102
+ label: "Force start (steal lock)",
103
+ description: "Start a new session, terminating the existing one.",
104
+ },
105
+ ],
106
+ notYetMessage: "Run /gsd when ready.",
107
+ });
108
+ if (choice === "status") {
109
+ await handleStatus(ctx);
110
+ return false;
111
+ }
112
+ if (choice === "steer") {
113
+ ctx.ui.notify("Use /gsd steer <instruction> to redirect the running auto-mode session.\n" +
114
+ "Example: /gsd steer Use Postgres instead of SQLite", "info");
115
+ return false;
116
+ }
117
+ if (choice === "stop") {
118
+ const result = stopAutoRemote(projectRoot());
119
+ if (result.found) {
120
+ ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
121
+ }
122
+ else if (result.error) {
123
+ ctx.ui.notify(`Failed to stop remote auto-mode: ${result.error}`, "error");
124
+ }
125
+ else {
126
+ ctx.ui.notify("Remote session is no longer running.", "info");
127
+ }
80
128
  return false;
81
- ctx.ui.notify(`Auto-mode is running in another process (PID ${remote.pid}).\n` +
82
- `Use these commands to interact with it:\n` +
83
- ` /gsd status check progress\n` +
84
- ` /gsd discuss — discuss architecture decisions\n` +
85
- ` /gsd queue queue the next milestone\n` +
86
- ` /gsd steer apply an override to active work\n` +
87
- ` /gsd capture — fire-and-forget thought\n` +
88
- ` /gsd stop — stop auto-mode`, "warning");
89
- return true;
129
+ }
130
+ if (choice === "force") {
131
+ return true; // Proceed startAuto will steal the lock
132
+ }
133
+ // "not_yet" or escape
134
+ return false;
90
135
  }
91
136
  export function registerGSDCommand(pi) {
92
137
  pi.registerCommand("gsd", {
@@ -542,12 +587,12 @@ export async function handleGSDCommand(args, ctx, pi) {
542
587
  await handleDryRun(ctx, projectRoot());
543
588
  return;
544
589
  }
545
- if (notifyRemoteAutoActive(ctx, projectRoot()))
546
- return;
547
590
  const verboseMode = trimmed.includes("--verbose");
548
591
  const debugMode = trimmed.includes("--debug");
549
592
  if (debugMode)
550
593
  enableDebug(projectRoot());
594
+ if (!(await guardRemoteSession(ctx, pi)))
595
+ return;
551
596
  await startAuto(ctx, pi, projectRoot(), verboseMode, { step: true });
552
597
  return;
553
598
  }
@@ -556,6 +601,8 @@ export async function handleGSDCommand(args, ctx, pi) {
556
601
  const debugMode = trimmed.includes("--debug");
557
602
  if (debugMode)
558
603
  enableDebug(projectRoot());
604
+ if (!(await guardRemoteSession(ctx, pi)))
605
+ return;
559
606
  await startAuto(ctx, pi, projectRoot(), verboseMode);
560
607
  return;
561
608
  }
@@ -899,7 +946,7 @@ Examples:
899
946
  return;
900
947
  }
901
948
  if (trimmed === "") {
902
- if (notifyRemoteAutoActive(ctx, projectRoot()))
949
+ if (!(await guardRemoteSession(ctx, pi)))
903
950
  return;
904
951
  await startAuto(ctx, pi, projectRoot(), false, { step: true });
905
952
  return;
@@ -629,6 +629,47 @@ export function countMustHavesMentionedInSummary(mustHaves, summaryContent) {
629
629
  }
630
630
  return count;
631
631
  }
632
+ // ─── Task Plan IO Extractor ────────────────────────────────────────────────
633
+ /**
634
+ * Extract input and output file paths from a task plan's `## Inputs` and
635
+ * `## Expected Output` sections. Looks for backtick-wrapped file paths on
636
+ * each line (e.g. `` `src/foo.ts` ``).
637
+ *
638
+ * Returns empty arrays for missing/empty sections — callers should treat
639
+ * tasks with no IO as ambiguous (sequential fallback trigger).
640
+ */
641
+ export function parseTaskPlanIO(content) {
642
+ const backtickPathRegex = /`([^`]+)`/g;
643
+ function extractPaths(sectionText) {
644
+ if (!sectionText)
645
+ return [];
646
+ const paths = [];
647
+ for (const line of sectionText.split("\n")) {
648
+ const trimmed = line.trim();
649
+ if (!trimmed || trimmed.startsWith("#"))
650
+ continue;
651
+ let match;
652
+ backtickPathRegex.lastIndex = 0;
653
+ while ((match = backtickPathRegex.exec(trimmed)) !== null) {
654
+ const candidate = match[1];
655
+ // Filter out things that look like code tokens rather than file paths
656
+ // (e.g. `true`, `false`, `npm run test`). A file path has at least one
657
+ // dot or slash.
658
+ if (candidate.includes("/") || candidate.includes(".")) {
659
+ paths.push(candidate);
660
+ }
661
+ }
662
+ }
663
+ return paths;
664
+ }
665
+ const [, body] = splitFrontmatter(content);
666
+ const inputSection = extractSection(body, "Inputs");
667
+ const outputSection = extractSection(body, "Expected Output");
668
+ return {
669
+ inputFiles: extractPaths(inputSection),
670
+ outputFiles: extractPaths(outputSection),
671
+ };
672
+ }
632
673
  /**
633
674
  * Extract the UAT type from a UAT file's raw content.
634
675
  *