gsd-pi 2.37.1 → 2.38.0-dev.e40f839

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 (155) hide show
  1. package/README.md +1 -1
  2. package/dist/app-paths.js +1 -1
  3. package/dist/cli.js +9 -0
  4. package/dist/extension-discovery.d.ts +5 -3
  5. package/dist/extension-discovery.js +14 -9
  6. package/dist/extension-registry.js +2 -2
  7. package/dist/onboarding.js +1 -0
  8. package/dist/remote-questions-config.js +2 -2
  9. package/dist/resources/extensions/browser-tools/package.json +3 -1
  10. package/dist/resources/extensions/cmux/index.js +55 -1
  11. package/dist/resources/extensions/context7/package.json +1 -1
  12. package/dist/resources/extensions/env-utils.js +29 -0
  13. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  14. package/dist/resources/extensions/google-search/package.json +3 -1
  15. package/dist/resources/extensions/gsd/auto-dispatch.js +67 -1
  16. package/dist/resources/extensions/gsd/auto-loop.js +7 -1
  17. package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
  18. package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
  19. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  20. package/dist/resources/extensions/gsd/auto-start.js +6 -1
  21. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  22. package/dist/resources/extensions/gsd/captures.js +9 -1
  23. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  24. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  25. package/dist/resources/extensions/gsd/commands.js +22 -2
  26. package/dist/resources/extensions/gsd/detection.js +1 -2
  27. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  28. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  29. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  30. package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
  31. package/dist/resources/extensions/gsd/doctor.js +184 -11
  32. package/dist/resources/extensions/gsd/export.js +1 -1
  33. package/dist/resources/extensions/gsd/files.js +43 -2
  34. package/dist/resources/extensions/gsd/forensics.js +1 -1
  35. package/dist/resources/extensions/gsd/index.js +2 -1
  36. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  37. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  38. package/dist/resources/extensions/gsd/package.json +1 -1
  39. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  40. package/dist/resources/extensions/gsd/preferences-validation.js +43 -1
  41. package/dist/resources/extensions/gsd/preferences.js +4 -3
  42. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  43. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  44. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  45. package/dist/resources/extensions/gsd/repo-identity.js +2 -1
  46. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  47. package/dist/resources/extensions/gsd/state.js +1 -1
  48. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  49. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  50. package/dist/resources/extensions/gsd/worktree.js +35 -16
  51. package/dist/resources/extensions/remote-questions/status.js +2 -1
  52. package/dist/resources/extensions/remote-questions/store.js +2 -1
  53. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  54. package/dist/resources/extensions/subagent/index.js +12 -3
  55. package/dist/resources/extensions/subagent/isolation.js +2 -1
  56. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  57. package/dist/resources/extensions/universal-config/package.json +1 -1
  58. package/dist/welcome-screen.d.ts +12 -0
  59. package/dist/welcome-screen.js +53 -0
  60. package/package.json +2 -1
  61. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  62. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  63. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  64. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  65. package/packages/pi-ai/dist/models.generated.js +172 -0
  66. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  67. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  68. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  69. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  70. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  71. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  72. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  73. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  74. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  75. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  76. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  78. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  79. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  80. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  81. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  82. package/packages/pi-ai/dist/types.d.ts +2 -2
  83. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  84. package/packages/pi-ai/dist/types.js.map +1 -1
  85. package/packages/pi-ai/package.json +1 -0
  86. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  87. package/packages/pi-ai/src/models.generated.ts +172 -0
  88. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  89. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  90. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  91. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  92. package/packages/pi-ai/src/types.ts +2 -0
  93. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  95. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  98. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  99. package/packages/pi-coding-agent/package.json +1 -1
  100. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  101. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  102. package/pkg/package.json +1 -1
  103. package/src/resources/extensions/cmux/index.ts +57 -1
  104. package/src/resources/extensions/env-utils.ts +31 -0
  105. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  106. package/src/resources/extensions/gsd/auto-dispatch.ts +93 -0
  107. package/src/resources/extensions/gsd/auto-loop.ts +13 -1
  108. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
  109. package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
  110. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  111. package/src/resources/extensions/gsd/auto-start.ts +7 -1
  112. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  113. package/src/resources/extensions/gsd/captures.ts +10 -1
  114. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  115. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  116. package/src/resources/extensions/gsd/commands.ts +24 -2
  117. package/src/resources/extensions/gsd/detection.ts +2 -2
  118. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  119. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  120. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  121. package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
  122. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  123. package/src/resources/extensions/gsd/doctor.ts +177 -13
  124. package/src/resources/extensions/gsd/export.ts +1 -1
  125. package/src/resources/extensions/gsd/files.ts +47 -2
  126. package/src/resources/extensions/gsd/forensics.ts +1 -1
  127. package/src/resources/extensions/gsd/index.ts +3 -1
  128. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  129. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  130. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  131. package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
  132. package/src/resources/extensions/gsd/preferences.ts +5 -3
  133. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  134. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  135. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  136. package/src/resources/extensions/gsd/repo-identity.ts +3 -1
  137. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  138. package/src/resources/extensions/gsd/state.ts +1 -1
  139. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  140. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  141. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  142. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
  143. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  144. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  145. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  146. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  147. package/src/resources/extensions/gsd/types.ts +43 -0
  148. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  149. package/src/resources/extensions/gsd/worktree.ts +35 -15
  150. package/src/resources/extensions/remote-questions/status.ts +3 -1
  151. package/src/resources/extensions/remote-questions/store.ts +3 -1
  152. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  153. package/src/resources/extensions/subagent/index.ts +12 -3
  154. package/src/resources/extensions/subagent/isolation.ts +3 -1
  155. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
@@ -289,10 +289,17 @@ export class CmuxClient {
289
289
  }
290
290
 
291
291
  async createSplit(direction: "right" | "down" | "left" | "up"): Promise<string | null> {
292
+ return this.createSplitFrom(this.config.surfaceId, direction);
293
+ }
294
+
295
+ async createSplitFrom(
296
+ sourceSurfaceId: string | undefined,
297
+ direction: "right" | "down" | "left" | "up",
298
+ ): Promise<string | null> {
292
299
  if (!this.config.splits) return null;
293
300
  const before = new Set(await this.listSurfaceIds());
294
301
  const args = ["new-split", direction];
295
- const scopedArgs = this.appendSurface(this.appendWorkspace(args), this.config.surfaceId);
302
+ const scopedArgs = this.appendSurface(this.appendWorkspace(args), sourceSurfaceId);
296
303
  await this.runAsync(scopedArgs);
297
304
  const after = await this.listSurfaceIds();
298
305
  for (const id of after) {
@@ -301,6 +308,55 @@ export class CmuxClient {
301
308
  return null;
302
309
  }
303
310
 
311
+ /**
312
+ * Create a grid of surfaces for parallel agent execution.
313
+ *
314
+ * Layout strategy (gsd stays in the original surface):
315
+ * 1 agent: [gsd | A]
316
+ * 2 agents: [gsd | A]
317
+ * [ | B]
318
+ * 3 agents: [gsd | A]
319
+ * [ C | B]
320
+ * 4 agents: [gsd | A]
321
+ * [ C | B] (D splits from B downward)
322
+ * [ | D]
323
+ *
324
+ * Returns surface IDs in order, or empty array on failure.
325
+ */
326
+ async createGridLayout(count: number): Promise<string[]> {
327
+ if (!this.config.splits || count <= 0) return [];
328
+ const surfaces: string[] = [];
329
+
330
+ // First split: create right column from the gsd surface
331
+ const rightCol = await this.createSplitFrom(this.config.surfaceId, "right");
332
+ if (!rightCol) return [];
333
+ surfaces.push(rightCol);
334
+ if (count === 1) return surfaces;
335
+
336
+ // Second split: split right column down → bottom-right
337
+ const bottomRight = await this.createSplitFrom(rightCol, "down");
338
+ if (!bottomRight) return surfaces;
339
+ surfaces.push(bottomRight);
340
+ if (count === 2) return surfaces;
341
+
342
+ // Third split: split gsd surface down → bottom-left
343
+ const bottomLeft = await this.createSplitFrom(this.config.surfaceId, "down");
344
+ if (!bottomLeft) return surfaces;
345
+ surfaces.push(bottomLeft);
346
+ if (count === 3) return surfaces;
347
+
348
+ // Fourth+: split subsequent surfaces down from the last created
349
+ let lastSurface = bottomRight;
350
+ for (let i = 3; i < count; i++) {
351
+ const next = await this.createSplitFrom(lastSurface, "down");
352
+ if (!next) break;
353
+ surfaces.push(next);
354
+ lastSurface = next;
355
+ }
356
+
357
+ return surfaces;
358
+ }
359
+
304
360
  async sendSurface(surfaceId: string, text: string): Promise<boolean> {
305
361
  const payload = text.endsWith("\n") ? text : `${text}\n`;
306
362
  const stdout = await this.runAsync(["send-surface", "--surface", surfaceId, payload]);
@@ -0,0 +1,31 @@
1
+ // GSD Extension — Environment variable utilities
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+ //
4
+ // Pure utility for checking existing env keys in .env files and process.env.
5
+ // Extracted from get-secrets-from-user.ts to avoid pulling in @gsd/pi-tui
6
+ // when only env-checking is needed (e.g. from files.ts during report generation).
7
+
8
+ import { readFile } from "node:fs/promises";
9
+
10
+ /**
11
+ * Check which keys already exist in a .env file or process.env.
12
+ * Returns the subset of `keys` that are already set.
13
+ */
14
+ export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
15
+ let fileContent = "";
16
+ try {
17
+ fileContent = await readFile(envFilePath, "utf8");
18
+ } catch {
19
+ // ENOENT or other read error — proceed with empty content
20
+ }
21
+
22
+ const existing: string[] = [];
23
+ for (const key of keys) {
24
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ const regex = new RegExp(`^${escaped}\\s*=`, "m");
26
+ if (regex.test(fileContent) || key in process.env) {
27
+ existing.push(key);
28
+ }
29
+ }
30
+ return existing;
31
+ }
@@ -67,30 +67,11 @@ async function writeEnvKey(filePath: string, key: string, value: string): Promis
67
67
 
68
68
  // ─── Exported utilities ───────────────────────────────────────────────────────
69
69
 
70
- /**
71
- * Check which keys already exist in the .env file or process.env.
72
- * Returns the subset of `keys` that are already set.
73
- * Handles ENOENT gracefully (still checks process.env).
74
- * Empty-string values count as existing.
75
- */
76
- export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
77
- let fileContent = "";
78
- try {
79
- fileContent = await readFile(envFilePath, "utf8");
80
- } catch {
81
- // ENOENT or other read error — proceed with empty content
82
- }
83
-
84
- const existing: string[] = [];
85
- for (const key of keys) {
86
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
87
- const regex = new RegExp(`^${escaped}\\s*=`, "m");
88
- if (regex.test(fileContent) || key in process.env) {
89
- existing.push(key);
90
- }
91
- }
92
- return existing;
93
- }
70
+ // Re-export from env-utils.ts so existing consumers still work.
71
+ // The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
72
+ // into modules that only need env-checking (e.g. files.ts during reports).
73
+ import { checkExistingEnvKeys } from "./env-utils.js";
74
+ export { checkExistingEnvKeys };
94
75
 
95
76
  /**
96
77
  * Detect the write destination based on project files in basePath.
@@ -38,6 +38,7 @@ import {
38
38
  buildRunUatPrompt,
39
39
  buildReassessRoadmapPrompt,
40
40
  buildRewriteDocsPrompt,
41
+ buildReactiveExecutePrompt,
41
42
  checkNeedsReassessment,
42
43
  checkNeedsRunUat,
43
44
  } from "./auto-prompts.js";
@@ -309,6 +310,98 @@ const DISPATCH_RULES: DispatchRule[] = [
309
310
  };
310
311
  },
311
312
  },
313
+ {
314
+ name: "executing → reactive-execute (parallel dispatch)",
315
+ match: async ({ state, mid, midTitle, basePath, prefs }) => {
316
+ if (state.phase !== "executing" || !state.activeTask) return null;
317
+ if (!state.activeSlice) return null; // fall through
318
+
319
+ // Only activate when reactive_execution is explicitly enabled
320
+ const reactiveConfig = prefs?.reactive_execution;
321
+ if (!reactiveConfig?.enabled) return null;
322
+
323
+ const sid = state.activeSlice.id;
324
+ const sTitle = state.activeSlice.title;
325
+ const maxParallel = reactiveConfig.max_parallel ?? 2;
326
+
327
+ // Dry-run mode: max_parallel=1 means graph is derived and logged but
328
+ // execution remains sequential
329
+ if (maxParallel <= 1) return null;
330
+
331
+ try {
332
+ const {
333
+ loadSliceTaskIO,
334
+ deriveTaskGraph,
335
+ isGraphAmbiguous,
336
+ getReadyTasks,
337
+ chooseNonConflictingSubset,
338
+ graphMetrics,
339
+ } = await import("./reactive-graph.js");
340
+
341
+ const taskIO = await loadSliceTaskIO(basePath, mid, sid);
342
+ if (taskIO.length < 2) return null; // single task, no point
343
+
344
+ const graph = deriveTaskGraph(taskIO);
345
+
346
+ // Ambiguous graph → fall through to sequential
347
+ if (isGraphAmbiguous(graph)) return null;
348
+
349
+ const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
350
+ const readyIds = getReadyTasks(graph, completed, new Set());
351
+
352
+ // Only activate reactive dispatch when >1 task is ready
353
+ if (readyIds.length <= 1) return null;
354
+
355
+ const selected = chooseNonConflictingSubset(
356
+ readyIds,
357
+ graph,
358
+ maxParallel,
359
+ new Set(),
360
+ );
361
+ if (selected.length <= 1) return null;
362
+
363
+ // Log graph metrics for observability
364
+ const metrics = graphMetrics(graph);
365
+ process.stderr.write(
366
+ `gsd-reactive: ${mid}/${sid} graph — tasks:${metrics.taskCount} edges:${metrics.edgeCount} ` +
367
+ `ready:${metrics.readySetSize} dispatching:${selected.length} ambiguous:${metrics.ambiguous}\n`,
368
+ );
369
+
370
+ // Persist dispatched batch so verification and recovery can check
371
+ // exactly which tasks were sent.
372
+ const { saveReactiveState } = await import("./reactive-graph.js");
373
+ saveReactiveState(basePath, mid, sid, {
374
+ sliceId: sid,
375
+ completed: [...completed],
376
+ dispatched: selected,
377
+ graphSnapshot: metrics,
378
+ updatedAt: new Date().toISOString(),
379
+ });
380
+
381
+ // Encode selected task IDs in unitId for artifact verification.
382
+ // Format: M001/S01/reactive+T02,T03
383
+ const batchSuffix = selected.join(",");
384
+
385
+ return {
386
+ action: "dispatch",
387
+ unitType: "reactive-execute",
388
+ unitId: `${mid}/${sid}/reactive+${batchSuffix}`,
389
+ prompt: await buildReactiveExecutePrompt(
390
+ mid,
391
+ midTitle,
392
+ sid,
393
+ sTitle,
394
+ selected,
395
+ basePath,
396
+ ),
397
+ };
398
+ } catch (err) {
399
+ // Non-fatal — fall through to sequential execution
400
+ process.stderr.write(`gsd-reactive: graph derivation failed: ${(err as Error).message}\n`);
401
+ return null;
402
+ }
403
+ },
404
+ },
312
405
  {
313
406
  name: "executing → execute-task (recover missing task plan → plan-slice)",
314
407
  match: async ({ state, mid, midTitle, basePath }) => {
@@ -787,7 +787,7 @@ export async function autoLoop(
787
787
  (m: { status: string }) =>
788
788
  m.status !== "complete" && m.status !== "parked",
789
789
  );
790
- if (incomplete.length === 0) {
790
+ if (incomplete.length === 0 && state.registry.length > 0) {
791
791
  // All milestones complete — merge milestone branch before stopping
792
792
  if (s.currentMilestoneId) {
793
793
  deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
@@ -804,6 +804,18 @@ export async function autoLoop(
804
804
  "success",
805
805
  );
806
806
  await deps.stopAuto(ctx, pi, "All milestones complete");
807
+ } else if (incomplete.length === 0 && state.registry.length === 0) {
808
+ // Empty registry — no milestones visible, likely a path resolution bug
809
+ const diag = `basePath=${s.basePath}, phase=${state.phase}`;
810
+ ctx.ui.notify(
811
+ `No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
812
+ "error",
813
+ );
814
+ await deps.stopAuto(
815
+ ctx,
816
+ pi,
817
+ `No milestones found — check basePath resolution`,
818
+ );
807
819
  } else if (state.phase === "blocked") {
808
820
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
809
821
  await deps.stopAuto(ctx, pi, blockerMsg);
@@ -217,6 +217,20 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
217
217
  }
218
218
  }
219
219
 
220
+ // Reactive state cleanup on slice completion
221
+ if (s.currentUnit.type === "complete-slice") {
222
+ try {
223
+ const parts = s.currentUnit.id.split("/");
224
+ const [mid, sid] = parts;
225
+ if (mid && sid) {
226
+ const { clearReactiveState } = await import("./reactive-graph.js");
227
+ clearReactiveState(s.basePath, mid, sid);
228
+ }
229
+ } catch {
230
+ // Non-fatal
231
+ }
232
+ }
233
+
220
234
  // Post-triage: execute actionable resolutions
221
235
  if (s.currentUnit.type === "triage-captures") {
222
236
  try {
@@ -485,6 +485,41 @@ export async function getPriorTaskSummaryPaths(
485
485
  .map(f => `${sRel}/tasks/${f}`);
486
486
  }
487
487
 
488
+ /**
489
+ * Get carry-forward summary paths scoped to a task's derived dependencies.
490
+ *
491
+ * Instead of all prior tasks (order-based), returns only summaries for task
492
+ * IDs in `dependsOn`. Used by reactive-execute to give each subagent only
493
+ * the context it actually needs — not sibling tasks from a parallel batch.
494
+ *
495
+ * Falls back to order-based when dependsOn is empty (root tasks still get
496
+ * any available prior summaries for continuity).
497
+ */
498
+ export async function getDependencyTaskSummaryPaths(
499
+ mid: string, sid: string, currentTid: string,
500
+ dependsOn: string[], base: string,
501
+ ): Promise<string[]> {
502
+ // If no dependencies, fall back to order-based for root tasks
503
+ if (dependsOn.length === 0) {
504
+ return getPriorTaskSummaryPaths(mid, sid, currentTid, base);
505
+ }
506
+
507
+ const tDir = resolveTasksDir(base, mid, sid);
508
+ if (!tDir) return [];
509
+
510
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
511
+ const sRel = relSlicePath(base, mid, sid);
512
+ const depSet = new Set(dependsOn.map((d) => d.toUpperCase()));
513
+
514
+ return summaryFiles
515
+ .filter((f) => {
516
+ // Extract task ID from filename: "T02-SUMMARY.md" → "T02"
517
+ const tid = f.replace(/-SUMMARY\.md$/i, "").toUpperCase();
518
+ return depSet.has(tid);
519
+ })
520
+ .map((f) => `${sRel}/tasks/${f}`);
521
+ }
522
+
488
523
  // ─── Adaptive Replanning Checks ────────────────────────────────────────────
489
524
 
490
525
  /**
@@ -772,13 +807,24 @@ export async function buildPlanSlicePrompt(
772
807
  });
773
808
  }
774
809
 
810
+ /** Options for customizing execute-task prompt construction. */
811
+ export interface ExecuteTaskPromptOptions {
812
+ level?: InlineLevel;
813
+ /** Override carry-forward paths (dependency-based instead of order-based). */
814
+ carryForwardPaths?: string[];
815
+ }
816
+
775
817
  export async function buildExecuteTaskPrompt(
776
818
  mid: string, sid: string, sTitle: string,
777
- tid: string, tTitle: string, base: string, level?: InlineLevel,
819
+ tid: string, tTitle: string, base: string,
820
+ level?: InlineLevel | ExecuteTaskPromptOptions,
778
821
  ): Promise<string> {
779
- const inlineLevel = level ?? resolveInlineLevel();
822
+ const opts: ExecuteTaskPromptOptions = typeof level === "object" && level !== null && !Array.isArray(level)
823
+ ? level
824
+ : { level: level as InlineLevel | undefined };
825
+ const inlineLevel = opts.level ?? resolveInlineLevel();
780
826
 
781
- const priorSummaries = await getPriorTaskSummaryPaths(mid, sid, tid, base);
827
+ const priorSummaries = opts.carryForwardPaths ?? await getPriorTaskSummaryPaths(mid, sid, tid, base);
782
828
  const priorLines = priorSummaries.length > 0
783
829
  ? priorSummaries.map(p => `- \`${p}\``).join("\n")
784
830
  : "- (no prior tasks)";
@@ -1234,6 +1280,82 @@ export async function buildReassessRoadmapPrompt(
1234
1280
  });
1235
1281
  }
1236
1282
 
1283
+ // ─── Reactive Execute Prompt ──────────────────────────────────────────────
1284
+
1285
+ export async function buildReactiveExecutePrompt(
1286
+ mid: string, midTitle: string, sid: string, sTitle: string,
1287
+ readyTaskIds: string[], base: string,
1288
+ ): Promise<string> {
1289
+ const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
1290
+
1291
+ // Build graph for context
1292
+ const taskIO = await loadSliceTaskIO(base, mid, sid);
1293
+ const graph = deriveTaskGraph(taskIO);
1294
+ const metrics = graphMetrics(graph);
1295
+
1296
+ // Build graph context section
1297
+ const graphLines: string[] = [];
1298
+ for (const node of graph) {
1299
+ const status = node.done ? "✅ done" : readyTaskIds.includes(node.id) ? "🟢 ready" : "⏳ waiting";
1300
+ const deps = node.dependsOn.length > 0 ? ` (depends on: ${node.dependsOn.join(", ")})` : "";
1301
+ graphLines.push(`- **${node.id}: ${node.title}** — ${status}${deps}`);
1302
+ if (node.outputFiles.length > 0) {
1303
+ graphLines.push(` - Outputs: ${node.outputFiles.map(f => `\`${f}\``).join(", ")}`);
1304
+ }
1305
+ }
1306
+ const graphContext = [
1307
+ `Tasks: ${metrics.taskCount}, Edges: ${metrics.edgeCount}, Ready: ${metrics.readySetSize}`,
1308
+ "",
1309
+ ...graphLines,
1310
+ ].join("\n");
1311
+
1312
+ // Build individual subagent prompts for each ready task
1313
+ const subagentSections: string[] = [];
1314
+ const readyTaskListLines: string[] = [];
1315
+
1316
+ for (const tid of readyTaskIds) {
1317
+ const node = graph.find((n) => n.id === tid);
1318
+ const tTitle = node?.title ?? tid;
1319
+ readyTaskListLines.push(`- **${tid}: ${tTitle}**`);
1320
+
1321
+ // Build dependency-scoped carry-forward paths for this task
1322
+ const depPaths = await getDependencyTaskSummaryPaths(
1323
+ mid, sid, tid, node?.dependsOn ?? [], base,
1324
+ );
1325
+
1326
+ // Build a full execute-task prompt with dependency-based carry-forward
1327
+ const taskPrompt = await buildExecuteTaskPrompt(
1328
+ mid, sid, sTitle, tid, tTitle, base,
1329
+ { carryForwardPaths: depPaths },
1330
+ );
1331
+
1332
+ subagentSections.push([
1333
+ `### ${tid}: ${tTitle}`,
1334
+ "",
1335
+ "Use this as the prompt for a `subagent` call:",
1336
+ "",
1337
+ "```",
1338
+ taskPrompt,
1339
+ "```",
1340
+ ].join("\n"));
1341
+ }
1342
+
1343
+ const inlinedTemplates = inlineTemplate("task-summary", "Task Summary");
1344
+
1345
+ return loadPrompt("reactive-execute", {
1346
+ workingDirectory: base,
1347
+ milestoneId: mid,
1348
+ milestoneTitle: midTitle,
1349
+ sliceId: sid,
1350
+ sliceTitle: sTitle,
1351
+ graphContext,
1352
+ readyTaskCount: String(readyTaskIds.length),
1353
+ readyTaskList: readyTaskListLines.join("\n"),
1354
+ subagentPrompts: subagentSections.join("\n\n---\n\n"),
1355
+ inlinedTemplates,
1356
+ });
1357
+ }
1358
+
1237
1359
  export async function buildRewriteDocsPrompt(
1238
1360
  mid: string, midTitle: string,
1239
1361
  activeSlice: { id: string; title: string } | null,
@@ -26,6 +26,7 @@ import {
26
26
  resolveSlicePath,
27
27
  resolveSliceFile,
28
28
  resolveTasksDir,
29
+ resolveTaskFiles,
29
30
  relMilestoneFile,
30
31
  relSliceFile,
31
32
  relSlicePath,
@@ -110,6 +111,9 @@ export function resolveExpectedArtifactPath(
110
111
  }
111
112
  case "rewrite-docs":
112
113
  return null;
114
+ case "reactive-execute":
115
+ // Reactive execute produces multiple task summaries — verified separately
116
+ return null;
113
117
  default:
114
118
  return null;
115
119
  }
@@ -148,6 +152,44 @@ export function verifyExpectedArtifact(
148
152
  return !content.includes("**Scope:** active");
149
153
  }
150
154
 
155
+ // Reactive-execute: verify that each dispatched task's summary exists.
156
+ // The unitId encodes the batch: "{mid}/{sid}/reactive+T02,T03"
157
+ if (unitType === "reactive-execute") {
158
+ const parts = unitId.split("/");
159
+ const mid = parts[0];
160
+ const sidAndBatch = parts[1];
161
+ const batchPart = parts[2]; // "reactive+T02,T03"
162
+ if (!mid || !sidAndBatch || !batchPart) return false;
163
+
164
+ const sid = sidAndBatch;
165
+ const plusIdx = batchPart.indexOf("+");
166
+ if (plusIdx === -1) {
167
+ // Legacy format "reactive" without batch IDs — fall back to "any summary"
168
+ const tDir = resolveTasksDir(base, mid, sid);
169
+ if (!tDir) return false;
170
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
171
+ return summaryFiles.length > 0;
172
+ }
173
+
174
+ const batchIds = batchPart.slice(plusIdx + 1).split(",").filter(Boolean);
175
+ if (batchIds.length === 0) return false;
176
+
177
+ const tDir = resolveTasksDir(base, mid, sid);
178
+ if (!tDir) return false;
179
+
180
+ const existingSummaries = new Set(
181
+ resolveTaskFiles(tDir, "SUMMARY").map((f) =>
182
+ f.replace(/-SUMMARY\.md$/i, "").toUpperCase(),
183
+ ),
184
+ );
185
+
186
+ // Every dispatched task must have a summary file
187
+ for (const tid of batchIds) {
188
+ if (!existingSummaries.has(tid.toUpperCase())) return false;
189
+ }
190
+ return true;
191
+ }
192
+
151
193
  const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
152
194
  // For unit types with no verifiable artifact (null path), the parent directory
153
195
  // is missing on disk — treat as stale completion state so the key gets evicted (#313).
@@ -429,10 +429,16 @@ export async function bootstrapAutoSession(
429
429
  s.originalBasePath = base;
430
430
 
431
431
  const isUnderGsdWorktrees = (p: string): boolean => {
432
+ // Direct layout: /.gsd/worktrees/
432
433
  const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
433
434
  if (p.includes(marker)) return true;
434
435
  const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
435
- return p.endsWith(worktreesSuffix);
436
+ if (p.endsWith(worktreesSuffix)) return true;
437
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
438
+ const symlinkRe = new RegExp(
439
+ `\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees(?:\\${pathSep}|$)`,
440
+ );
441
+ return symlinkRe.test(p);
436
442
  };
437
443
 
438
444
  if (
@@ -22,6 +22,8 @@ import { join, sep as pathSep } from "node:path";
22
22
  import { homedir } from "node:os";
23
23
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
24
24
 
25
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
26
+
25
27
  // ─── Project Root → Worktree Sync ─────────────────────────────────────────
26
28
 
27
29
  /**
@@ -111,7 +113,7 @@ export function syncStateToProjectRoot(
111
113
  */
112
114
  export function readResourceVersion(): string | null {
113
115
  const agentDir =
114
- process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
116
+ process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
115
117
  const manifestPath = join(agentDir, "managed-resources.json");
116
118
  try {
117
119
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -153,9 +155,18 @@ export function checkResourcesStale(
153
155
  * Returns the corrected base path.
154
156
  */
155
157
  export function escapeStaleWorktree(base: string): string {
156
- const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
157
- const idx = base.indexOf(marker);
158
- if (idx === -1) return base;
158
+ // Direct layout: /.gsd/worktrees/
159
+ const directMarker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
160
+ let idx = base.indexOf(directMarker);
161
+ if (idx === -1) {
162
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
163
+ const symlinkRe = new RegExp(
164
+ `\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees\\${pathSep}`,
165
+ );
166
+ const match = base.match(symlinkRe);
167
+ if (!match || match.index === undefined) return base;
168
+ idx = match.index;
169
+ }
159
170
 
160
171
  // base is inside .gsd/worktrees/<something> — extract the project root
161
172
  const projectRoot = base.slice(0, idx);
@@ -59,8 +59,17 @@ const VALID_CLASSIFICATIONS: readonly string[] = [
59
59
  */
60
60
  export function resolveCapturesPath(basePath: string): string {
61
61
  const resolved = resolve(basePath);
62
+ // Direct layout: /.gsd/worktrees/
62
63
  const worktreeMarker = `${sep}.gsd${sep}worktrees${sep}`;
63
- const idx = resolved.indexOf(worktreeMarker);
64
+ let idx = resolved.indexOf(worktreeMarker);
65
+ if (idx === -1) {
66
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
67
+ const symlinkRe = new RegExp(
68
+ `\\${sep}\\.gsd\\${sep}projects\\${sep}[a-f0-9]+\\${sep}worktrees\\${sep}`,
69
+ );
70
+ const match = resolved.match(symlinkRe);
71
+ if (match && match.index !== undefined) idx = match.index;
72
+ }
64
73
  if (idx !== -1) {
65
74
  // basePath is inside a worktree — resolve to project root
66
75
  const projectRoot = resolved.slice(0, idx);
@@ -11,6 +11,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFile
11
11
  import { dirname, join } from "node:path";
12
12
  import { homedir } from "node:os";
13
13
 
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
+
14
16
  // ─── Types (mirrored from extension-registry.ts) ────────────────────────────
15
17
 
16
18
  interface ExtensionManifest {
@@ -48,11 +50,11 @@ interface ExtensionRegistry {
48
50
  // ─── Registry I/O ───────────────────────────────────────────────────────────
49
51
 
50
52
  function getRegistryPath(): string {
51
- return join(homedir(), ".gsd", "extensions", "registry.json");
53
+ return join(gsdHome, "extensions", "registry.json");
52
54
  }
53
55
 
54
56
  function getAgentExtensionsDir(): string {
55
- return join(homedir(), ".gsd", "agent", "extensions");
57
+ return join(gsdHome, "agent", "extensions");
56
58
  }
57
59
 
58
60
  function loadRegistry(): ExtensionRegistry {
@@ -15,6 +15,7 @@ import { appendOverride, appendKnowledge } from "./files.js";
15
15
  import {
16
16
  formatDoctorIssuesForPrompt,
17
17
  formatDoctorReport,
18
+ formatDoctorReportJson,
18
19
  runGSDDoctor,
19
20
  selectDoctorScope,
20
21
  filterDoctorIssues,
@@ -43,16 +44,30 @@ export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined,
43
44
 
44
45
  export async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
45
46
  const trimmed = args.trim();
46
- const parts = trimmed ? trimmed.split(/\s+/) : [];
47
+ // Extract flags before positional parsing
48
+ const jsonMode = trimmed.includes("--json");
49
+ const dryRun = trimmed.includes("--dry-run");
50
+ const includeBuild = trimmed.includes("--build");
51
+ const includeTests = trimmed.includes("--test");
52
+ const stripped = trimmed.replace(/--json|--dry-run|--build|--test/g, "").trim();
53
+ const parts = stripped ? stripped.split(/\s+/) : [];
47
54
  const mode = parts[0] === "fix" || parts[0] === "heal" || parts[0] === "audit" ? parts[0] : "doctor";
48
55
  const requestedScope = mode === "doctor" ? parts[0] : parts[1];
49
56
  const scope = await selectDoctorScope(projectRoot(), requestedScope);
50
57
  const effectiveScope = mode === "audit" ? requestedScope : scope;
51
58
  const report = await runGSDDoctor(projectRoot(), {
52
- fix: mode === "fix" || mode === "heal",
59
+ fix: mode === "fix" || mode === "heal" || dryRun,
60
+ dryRun,
53
61
  scope: effectiveScope,
62
+ includeBuild,
63
+ includeTests,
54
64
  });
55
65
 
66
+ if (jsonMode) {
67
+ ctx.ui.notify(formatDoctorReportJson(report), "info");
68
+ return;
69
+ }
70
+
56
71
  const reportText = formatDoctorReport(report, {
57
72
  scope: effectiveScope,
58
73
  includeWarnings: mode === "audit",