gsd-pi 2.37.1 → 2.38.0-dev.add4f78

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 (159) 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/session.js +3 -0
  16. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
  17. package/dist/resources/extensions/gsd/auto-loop.js +61 -31
  18. package/dist/resources/extensions/gsd/auto-post-unit.js +87 -69
  19. package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
  20. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  21. package/dist/resources/extensions/gsd/auto-start.js +6 -1
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  23. package/dist/resources/extensions/gsd/auto.js +10 -26
  24. package/dist/resources/extensions/gsd/captures.js +9 -1
  25. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  26. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  27. package/dist/resources/extensions/gsd/commands.js +22 -2
  28. package/dist/resources/extensions/gsd/detection.js +1 -2
  29. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  30. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  31. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  32. package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
  33. package/dist/resources/extensions/gsd/doctor.js +184 -11
  34. package/dist/resources/extensions/gsd/export.js +1 -1
  35. package/dist/resources/extensions/gsd/files.js +43 -2
  36. package/dist/resources/extensions/gsd/forensics.js +1 -1
  37. package/dist/resources/extensions/gsd/index.js +2 -1
  38. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  39. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  40. package/dist/resources/extensions/gsd/package.json +1 -1
  41. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  42. package/dist/resources/extensions/gsd/preferences-validation.js +43 -1
  43. package/dist/resources/extensions/gsd/preferences.js +4 -3
  44. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  45. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  46. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  47. package/dist/resources/extensions/gsd/repo-identity.js +2 -1
  48. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  49. package/dist/resources/extensions/gsd/state.js +1 -1
  50. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  51. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  52. package/dist/resources/extensions/gsd/worktree.js +35 -16
  53. package/dist/resources/extensions/remote-questions/status.js +2 -1
  54. package/dist/resources/extensions/remote-questions/store.js +2 -1
  55. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  56. package/dist/resources/extensions/subagent/index.js +12 -3
  57. package/dist/resources/extensions/subagent/isolation.js +2 -1
  58. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  59. package/dist/resources/extensions/universal-config/package.json +1 -1
  60. package/dist/welcome-screen.d.ts +12 -0
  61. package/dist/welcome-screen.js +53 -0
  62. package/package.json +2 -1
  63. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  64. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  65. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  66. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  67. package/packages/pi-ai/dist/models.generated.js +172 -0
  68. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  69. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  70. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  71. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  72. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  73. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  74. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  75. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  76. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  77. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  78. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  80. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  82. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  83. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  84. package/packages/pi-ai/dist/types.d.ts +2 -2
  85. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  86. package/packages/pi-ai/dist/types.js.map +1 -1
  87. package/packages/pi-ai/package.json +1 -0
  88. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  89. package/packages/pi-ai/src/models.generated.ts +172 -0
  90. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  91. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  92. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  93. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  94. package/packages/pi-ai/src/types.ts +2 -0
  95. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  97. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  100. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  101. package/packages/pi-coding-agent/package.json +1 -1
  102. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  103. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  104. package/pkg/package.json +1 -1
  105. package/src/resources/extensions/cmux/index.ts +57 -1
  106. package/src/resources/extensions/env-utils.ts +31 -0
  107. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  108. package/src/resources/extensions/gsd/auto/session.ts +5 -1
  109. package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
  110. package/src/resources/extensions/gsd/auto-loop.ts +83 -64
  111. package/src/resources/extensions/gsd/auto-post-unit.ts +64 -40
  112. package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
  113. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  114. package/src/resources/extensions/gsd/auto-start.ts +7 -1
  115. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  116. package/src/resources/extensions/gsd/auto.ts +14 -29
  117. package/src/resources/extensions/gsd/captures.ts +10 -1
  118. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  119. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  120. package/src/resources/extensions/gsd/commands.ts +24 -2
  121. package/src/resources/extensions/gsd/detection.ts +2 -2
  122. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  123. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  124. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  125. package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
  126. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  127. package/src/resources/extensions/gsd/doctor.ts +177 -13
  128. package/src/resources/extensions/gsd/export.ts +1 -1
  129. package/src/resources/extensions/gsd/files.ts +47 -2
  130. package/src/resources/extensions/gsd/forensics.ts +1 -1
  131. package/src/resources/extensions/gsd/index.ts +3 -1
  132. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  133. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  134. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  135. package/src/resources/extensions/gsd/preferences-validation.ts +42 -1
  136. package/src/resources/extensions/gsd/preferences.ts +5 -3
  137. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  138. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  139. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  140. package/src/resources/extensions/gsd/repo-identity.ts +3 -1
  141. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  142. package/src/resources/extensions/gsd/state.ts +1 -1
  143. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  144. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  145. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  146. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
  147. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  148. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  149. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  150. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  151. package/src/resources/extensions/gsd/types.ts +43 -0
  152. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  153. package/src/resources/extensions/gsd/worktree.ts +35 -15
  154. package/src/resources/extensions/remote-questions/status.ts +3 -1
  155. package/src/resources/extensions/remote-questions/store.ts +3 -1
  156. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  157. package/src/resources/extensions/subagent/index.ts +12 -3
  158. package/src/resources/extensions/subagent/isolation.ts +3 -1
  159. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
@@ -33,7 +33,6 @@ import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.j
33
33
  import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
34
34
  import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
35
35
  import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
36
- import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
37
36
  import { isDbAvailable } from "./gsd-db.js";
38
37
  import { consumeSignal } from "./session-status-io.js";
39
38
  import {
@@ -56,6 +55,13 @@ import { join } from "node:path";
56
55
  /** Throttle STATE.md rebuilds — at most once per 30 seconds */
57
56
  const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
58
57
 
58
+ export interface PreVerificationOpts {
59
+ skipSettleDelay?: boolean;
60
+ skipDoctor?: boolean;
61
+ skipStateRebuild?: boolean;
62
+ skipWorktreeSync?: boolean;
63
+ }
64
+
59
65
  export interface PostUnitContext {
60
66
  s: AutoSession;
61
67
  ctx: ExtensionContext;
@@ -73,7 +79,7 @@ export interface PostUnitContext {
73
79
  *
74
80
  * Returns "dispatched" if a signal caused stop/pause, "continue" to proceed.
75
81
  */
76
- export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"dispatched" | "continue"> {
82
+ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreVerificationOpts): Promise<"dispatched" | "continue"> {
77
83
  const { s, ctx, pi, buildSnapshotOpts, stopAuto, pauseAuto } = pctx;
78
84
 
79
85
  // ── Parallel worker signal check ──
@@ -95,8 +101,10 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
95
101
  // Invalidate all caches
96
102
  invalidateAllCaches();
97
103
 
98
- // Small delay to let files settle
99
- await new Promise(r => setTimeout(r, 500));
104
+ // Small delay to let files settle (skipped for sidecars where latency matters more)
105
+ if (!opts?.skipSettleDelay) {
106
+ await new Promise(r => setTimeout(r, 100));
107
+ }
100
108
 
101
109
  // Auto-commit
102
110
  if (s.currentUnit) {
@@ -120,8 +128,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
120
128
  keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
121
129
  };
122
130
  }
123
- } catch {
124
- // Non-fatal
131
+ } catch (e) {
132
+ debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
125
133
  }
126
134
  }
127
135
  }
@@ -131,12 +139,12 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
131
139
  if (commitMsg) {
132
140
  ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
133
141
  }
134
- } catch {
135
- // Non-fatal
142
+ } catch (e) {
143
+ debugLog("postUnit", { phase: "auto-commit", error: String(e) });
136
144
  }
137
145
 
138
- // Doctor: fix mechanical bookkeeping
139
- try {
146
+ // Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
147
+ if (!opts?.skipDoctor) try {
140
148
  const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
141
149
  const doctorScope = scopeParts.join("/");
142
150
  const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
@@ -168,24 +176,26 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
168
176
  const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
169
177
  const structuredIssues = formatDoctorIssuesForPrompt(actionable);
170
178
  dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
171
- } catch {
172
- // Non-fatal
179
+ } catch (e) {
180
+ debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
173
181
  }
174
182
  }
175
183
  }
176
- } catch {
177
- // Non-fatal
184
+ } catch (e) {
185
+ debugLog("postUnit", { phase: "doctor", error: String(e) });
178
186
  }
179
187
 
180
- // Throttled STATE.md rebuild
181
- const now = Date.now();
182
- if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
183
- try {
184
- await rebuildState(s.basePath);
185
- s.lastStateRebuildAt = now;
186
- autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
187
- } catch {
188
- // Non-fatal
188
+ // Throttled STATE.md rebuild (skipped for lightweight sidecars)
189
+ if (!opts?.skipStateRebuild) {
190
+ const now = Date.now();
191
+ if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
192
+ try {
193
+ await rebuildState(s.basePath);
194
+ s.lastStateRebuildAt = now;
195
+ autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
196
+ } catch (e) {
197
+ debugLog("postUnit", { phase: "state-rebuild", error: String(e) });
198
+ }
189
199
  }
190
200
  }
191
201
 
@@ -193,16 +203,16 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
193
203
  try {
194
204
  const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
195
205
  pruneDeadProcesses();
196
- } catch {
197
- // Non-fatal
206
+ } catch (e) {
207
+ debugLog("postUnit", { phase: "prune-bg-shell", error: String(e) });
198
208
  }
199
209
 
200
- // Sync worktree state back to project root
201
- if (s.originalBasePath && s.originalBasePath !== s.basePath) {
210
+ // Sync worktree state back to project root (skipped for lightweight sidecars)
211
+ if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
202
212
  try {
203
213
  syncStateToProjectRoot(s.basePath, s.originalBasePath, s.currentMilestoneId);
204
- } catch {
205
- // Non-fatal
214
+ } catch (e) {
215
+ debugLog("postUnit", { phase: "worktree-sync", error: String(e) });
206
216
  }
207
217
  }
208
218
 
@@ -210,10 +220,24 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
210
220
  if (s.currentUnit.type === "rewrite-docs") {
211
221
  try {
212
222
  await resolveAllOverrides(s.basePath);
213
- resetRewriteCircuitBreaker();
223
+ s.rewriteAttemptCount = 0;
214
224
  ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
215
- } catch {
216
- // Non-fatal
225
+ } catch (e) {
226
+ debugLog("postUnit", { phase: "rewrite-docs-resolve", error: String(e) });
227
+ }
228
+ }
229
+
230
+ // Reactive state cleanup on slice completion
231
+ if (s.currentUnit.type === "complete-slice") {
232
+ try {
233
+ const parts = s.currentUnit.id.split("/");
234
+ const [mid, sid] = parts;
235
+ if (mid && sid) {
236
+ const { clearReactiveState } = await import("./reactive-graph.js");
237
+ clearReactiveState(s.basePath, mid, sid);
238
+ }
239
+ } catch (e) {
240
+ debugLog("postUnit", { phase: "reactive-state-cleanup", error: String(e) });
217
241
  }
218
242
  }
219
243
 
@@ -266,8 +290,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
266
290
  if (triggerArtifactVerified) {
267
291
  invalidateAllCaches();
268
292
  }
269
- } catch {
270
- // Non-fatal
293
+ } catch (e) {
294
+ debugLog("postUnit", { phase: "artifact-verify", error: String(e) });
271
295
  }
272
296
  } else {
273
297
  // Hook unit completed — finalize its runtime record
@@ -278,8 +302,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
278
302
  lastProgressKind: "hook-completed",
279
303
  });
280
304
  clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
281
- } catch {
282
- // Non-fatal
305
+ } catch (e) {
306
+ debugLog("postUnit", { phase: "hook-finalize", error: String(e) });
283
307
  }
284
308
  }
285
309
  }
@@ -415,8 +439,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
415
439
  }
416
440
  }
417
441
  }
418
- } catch {
419
- // Triage check failure is non-fatal
442
+ } catch (e) {
443
+ debugLog("postUnit", { phase: "triage-check", error: String(e) });
420
444
  }
421
445
  }
422
446
 
@@ -461,8 +485,8 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
461
485
  );
462
486
 
463
487
  return "continue";
464
- } catch {
465
- // Non-fatal proceed to normal dispatch
488
+ } catch (e) {
489
+ debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
466
490
  }
467
491
  }
468
492
 
@@ -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);
@@ -622,43 +622,28 @@ export async function stopAuto(
622
622
  if (existsSync(pausedPath)) unlinkSync(pausedPath);
623
623
  } catch { /* non-fatal */ }
624
624
 
625
- s.active = false;
626
- s.paused = false;
627
- s.stepMode = false;
628
- s.unitDispatchCount.clear();
629
- s.unitRecoveryCount.clear();
630
- clearInFlightTools();
631
- s.lastBudgetAlertLevel = 0;
632
- s.lastStateRebuildAt = 0;
633
- s.unitLifetimeDispatches.clear();
634
- s.currentUnit = null;
635
- s.autoModeStartModel = null;
636
- s.currentMilestoneId = null;
637
- s.originalBasePath = "";
638
- s.completedUnits = [];
639
- s.pendingQuickTasks = [];
640
- clearSliceProgressCache();
641
- clearActivityLogState();
642
- resetProactiveHealing();
643
- s.pendingCrashRecovery = null;
644
- s.pendingVerificationRetry = null;
645
- s.verificationRetryCount.clear();
646
- s.pausedSessionFile = null;
647
- ctx?.ui.setStatus("gsd-auto", undefined);
648
- ctx?.ui.setWidget("gsd-progress", undefined);
649
- ctx?.ui.setFooter(undefined);
650
-
625
+ // Restore original model before reset() clears the IDs
651
626
  if (pi && ctx && s.originalModelId && s.originalModelProvider) {
652
627
  const original = ctx.modelRegistry.find(
653
628
  s.originalModelProvider,
654
629
  s.originalModelId,
655
630
  );
656
631
  if (original) await pi.setModel(original);
657
- s.originalModelId = null;
658
- s.originalModelProvider = null;
659
632
  }
660
633
 
661
- s.cmdCtx = null;
634
+ // External cleanup (not covered by session reset)
635
+ clearInFlightTools();
636
+ clearSliceProgressCache();
637
+ clearActivityLogState();
638
+ resetProactiveHealing();
639
+
640
+ // UI cleanup
641
+ ctx?.ui.setStatus("gsd-auto", undefined);
642
+ ctx?.ui.setWidget("gsd-progress", undefined);
643
+ ctx?.ui.setFooter(undefined);
644
+
645
+ // Reset all session state in one call
646
+ s.reset();
662
647
  }
663
648
 
664
649
  /**
@@ -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",