gsd-pi 2.37.1 → 2.38.0-dev.29edcdc

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 (239) 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/resource-loader.js +34 -1
  10. package/dist/resources/extensions/browser-tools/package.json +3 -1
  11. package/dist/resources/extensions/cmux/index.js +55 -1
  12. package/dist/resources/extensions/context7/package.json +1 -1
  13. package/dist/resources/extensions/env-utils.js +29 -0
  14. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  15. package/dist/resources/extensions/github-sync/cli.js +284 -0
  16. package/dist/resources/extensions/github-sync/index.js +73 -0
  17. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  18. package/dist/resources/extensions/github-sync/sync.js +424 -0
  19. package/dist/resources/extensions/github-sync/templates.js +118 -0
  20. package/dist/resources/extensions/github-sync/types.js +7 -0
  21. package/dist/resources/extensions/google-search/package.json +3 -1
  22. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  23. package/dist/resources/extensions/gsd/auto-dispatch.js +75 -10
  24. package/dist/resources/extensions/gsd/auto-loop.js +597 -588
  25. package/dist/resources/extensions/gsd/auto-post-unit.js +111 -68
  26. package/dist/resources/extensions/gsd/auto-prompts.js +114 -45
  27. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  28. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  29. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  30. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  31. package/dist/resources/extensions/gsd/auto.js +143 -96
  32. package/dist/resources/extensions/gsd/captures.js +9 -1
  33. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  34. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  35. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  36. package/dist/resources/extensions/gsd/commands.js +24 -3
  37. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  38. package/dist/resources/extensions/gsd/detection.js +1 -2
  39. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  40. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  41. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  42. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  43. package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
  44. package/dist/resources/extensions/gsd/doctor.js +204 -12
  45. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  46. package/dist/resources/extensions/gsd/export.js +1 -1
  47. package/dist/resources/extensions/gsd/files.js +47 -2
  48. package/dist/resources/extensions/gsd/forensics.js +1 -1
  49. package/dist/resources/extensions/gsd/git-service.js +15 -12
  50. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  51. package/dist/resources/extensions/gsd/index.js +24 -20
  52. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  53. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  54. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  55. package/dist/resources/extensions/gsd/package.json +1 -1
  56. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  57. package/dist/resources/extensions/gsd/preferences-types.js +3 -2
  58. package/dist/resources/extensions/gsd/preferences-validation.js +101 -11
  59. package/dist/resources/extensions/gsd/preferences.js +8 -5
  60. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  61. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  62. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  63. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  64. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  65. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  66. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  67. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  68. package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
  69. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  70. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  71. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  72. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  73. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  74. package/dist/resources/extensions/gsd/state.js +1 -1
  75. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  76. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  77. package/dist/resources/extensions/gsd/worktree.js +35 -16
  78. package/dist/resources/extensions/mcp-client/index.js +14 -1
  79. package/dist/resources/extensions/remote-questions/status.js +2 -1
  80. package/dist/resources/extensions/remote-questions/store.js +2 -1
  81. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  82. package/dist/resources/extensions/subagent/index.js +12 -3
  83. package/dist/resources/extensions/subagent/isolation.js +2 -1
  84. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  85. package/dist/resources/extensions/universal-config/package.json +1 -1
  86. package/dist/welcome-screen.d.ts +12 -0
  87. package/dist/welcome-screen.js +53 -0
  88. package/package.json +2 -1
  89. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  90. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  91. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  92. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  93. package/packages/pi-ai/dist/models.generated.js +172 -0
  94. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  95. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  96. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  97. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  98. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  99. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  100. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  101. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  102. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  103. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  104. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  106. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  109. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  110. package/packages/pi-ai/dist/types.d.ts +2 -2
  111. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  112. package/packages/pi-ai/dist/types.js.map +1 -1
  113. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  114. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  115. package/packages/pi-ai/package.json +1 -0
  116. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  117. package/packages/pi-ai/src/models.generated.ts +172 -0
  118. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  119. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  120. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  121. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  122. package/packages/pi-ai/src/types.ts +2 -0
  123. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  124. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  126. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  129. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  132. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  133. package/packages/pi-coding-agent/package.json +1 -1
  134. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  135. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  136. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  137. package/pkg/package.json +1 -1
  138. package/src/resources/extensions/cmux/index.ts +57 -1
  139. package/src/resources/extensions/env-utils.ts +31 -0
  140. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  141. package/src/resources/extensions/github-sync/cli.ts +364 -0
  142. package/src/resources/extensions/github-sync/index.ts +93 -0
  143. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  144. package/src/resources/extensions/github-sync/sync.ts +556 -0
  145. package/src/resources/extensions/github-sync/templates.ts +183 -0
  146. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  147. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  148. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  149. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  150. package/src/resources/extensions/github-sync/types.ts +47 -0
  151. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  152. package/src/resources/extensions/gsd/auto-dispatch.ts +100 -9
  153. package/src/resources/extensions/gsd/auto-loop.ts +484 -546
  154. package/src/resources/extensions/gsd/auto-post-unit.ts +92 -42
  155. package/src/resources/extensions/gsd/auto-prompts.ts +150 -48
  156. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  157. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  158. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  159. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  160. package/src/resources/extensions/gsd/auto.ts +139 -101
  161. package/src/resources/extensions/gsd/captures.ts +10 -1
  162. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  163. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  164. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  165. package/src/resources/extensions/gsd/commands.ts +26 -4
  166. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  167. package/src/resources/extensions/gsd/detection.ts +2 -2
  168. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  169. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  170. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  171. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  172. package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
  173. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  174. package/src/resources/extensions/gsd/doctor.ts +199 -14
  175. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  176. package/src/resources/extensions/gsd/export.ts +1 -1
  177. package/src/resources/extensions/gsd/files.ts +50 -3
  178. package/src/resources/extensions/gsd/forensics.ts +1 -1
  179. package/src/resources/extensions/gsd/git-service.ts +20 -10
  180. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  181. package/src/resources/extensions/gsd/index.ts +24 -17
  182. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  183. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  184. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  185. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  186. package/src/resources/extensions/gsd/preferences-types.ts +9 -5
  187. package/src/resources/extensions/gsd/preferences-validation.ts +92 -11
  188. package/src/resources/extensions/gsd/preferences.ts +8 -5
  189. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  190. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  191. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  192. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  193. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  194. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  195. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  196. package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  197. package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
  198. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  199. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  200. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  201. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  202. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  203. package/src/resources/extensions/gsd/state.ts +1 -1
  204. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  205. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  206. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  207. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  208. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  209. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
  210. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  211. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  212. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  213. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  214. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  215. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  216. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  217. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  218. package/src/resources/extensions/gsd/types.ts +43 -1
  219. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  220. package/src/resources/extensions/gsd/worktree.ts +35 -15
  221. package/src/resources/extensions/mcp-client/index.ts +17 -1
  222. package/src/resources/extensions/remote-questions/status.ts +3 -1
  223. package/src/resources/extensions/remote-questions/store.ts +3 -1
  224. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  225. package/src/resources/extensions/subagent/index.ts +12 -3
  226. package/src/resources/extensions/subagent/isolation.ts +3 -1
  227. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  228. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  229. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  230. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  231. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  232. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  233. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  234. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  235. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  236. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  237. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  238. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  239. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -22,7 +22,6 @@ import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.j
22
22
  import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
23
23
  import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
24
24
  import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
25
- import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
26
25
  import { isDbAvailable } from "./gsd-db.js";
27
26
  import { consumeSignal } from "./session-status-io.js";
28
27
  import { checkPostUnitHooks, isRetryPending, consumeRetryTrigger, persistHookState, } from "./post-unit-hooks.js";
@@ -36,7 +35,7 @@ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
36
35
  *
37
36
  * Returns "dispatched" if a signal caused stop/pause, "continue" to proceed.
38
37
  */
39
- export async function postUnitPreVerification(pctx) {
38
+ export async function postUnitPreVerification(pctx, opts) {
40
39
  const { s, ctx, pi, buildSnapshotOpts, stopAuto, pauseAuto } = pctx;
41
40
  // ── Parallel worker signal check ──
42
41
  const milestoneLock = process.env.GSD_MILESTONE_LOCK;
@@ -55,8 +54,10 @@ export async function postUnitPreVerification(pctx) {
55
54
  }
56
55
  // Invalidate all caches
57
56
  invalidateAllCaches();
58
- // Small delay to let files settle
59
- await new Promise(r => setTimeout(r, 500));
57
+ // Small delay to let files settle (skipped for sidecars where latency matters more)
58
+ if (!opts?.skipSettleDelay) {
59
+ await new Promise(r => setTimeout(r, 100));
60
+ }
60
61
  // Auto-commit
61
62
  if (s.currentUnit) {
62
63
  try {
@@ -71,16 +72,26 @@ export async function postUnitPreVerification(pctx) {
71
72
  const summaryContent = await loadFile(summaryPath);
72
73
  if (summaryContent) {
73
74
  const summary = parseSummary(summaryContent);
75
+ // Look up GitHub issue number for commit linking
76
+ let ghIssueNumber;
77
+ try {
78
+ const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
79
+ ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
80
+ }
81
+ catch {
82
+ // GitHub sync not available — skip
83
+ }
74
84
  taskContext = {
75
85
  taskId: `${sid}/${tid}`,
76
86
  taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
77
87
  oneLiner: summary.oneLiner || undefined,
78
88
  keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
89
+ issueNumber: ghIssueNumber,
79
90
  };
80
91
  }
81
92
  }
82
- catch {
83
- // Non-fatal
93
+ catch (e) {
94
+ debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
84
95
  }
85
96
  }
86
97
  }
@@ -90,57 +101,75 @@ export async function postUnitPreVerification(pctx) {
90
101
  ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
91
102
  }
92
103
  }
93
- catch {
94
- // Non-fatal
104
+ catch (e) {
105
+ debugLog("postUnit", { phase: "auto-commit", error: String(e) });
95
106
  }
96
- // Doctor: fix mechanical bookkeeping
107
+ // GitHub sync (non-blocking, opt-in)
97
108
  try {
98
- const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
99
- const doctorScope = scopeParts.join("/");
100
- const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
101
- const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" : "task";
102
- const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
103
- if (report.fixesApplied.length > 0) {
104
- ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
105
- }
106
- // Proactive health tracking
107
- const summary = summarizeDoctorIssues(report.issues);
108
- recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
109
- // Check if we should escalate to LLM-assisted heal
110
- if (summary.errors > 0) {
111
- const unresolvedErrors = report.issues
112
- .filter(i => i.severity === "error" && !i.fixable)
113
- .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
114
- const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
115
- if (escalation.shouldEscalate) {
116
- ctx.ui.notify(`Doctor heal escalation: ${escalation.reason}. Dispatching LLM-assisted heal.`, "warning");
117
- try {
118
- const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
119
- const { dispatchDoctorHeal } = await import("./commands-handlers.js");
120
- const actionable = report.issues.filter(i => i.severity === "error");
121
- const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
122
- const structuredIssues = formatDoctorIssuesForPrompt(actionable);
123
- dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
124
- }
125
- catch {
126
- // Non-fatal
127
- }
128
- }
129
- }
109
+ const { runGitHubSync } = await import("../github-sync/sync.js");
110
+ await runGitHubSync(s.basePath, s.currentUnit.type, s.currentUnit.id);
130
111
  }
131
- catch {
132
- // Non-fatal
112
+ catch (e) {
113
+ debugLog("postUnit", { phase: "github-sync", error: String(e) });
133
114
  }
134
- // Throttled STATE.md rebuild
135
- const now = Date.now();
136
- if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
115
+ // Doctor: fix mechanical bookkeeping (skipped for lightweight sidecars)
116
+ if (!opts?.skipDoctor)
137
117
  try {
138
- await rebuildState(s.basePath);
139
- s.lastStateRebuildAt = now;
140
- autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
118
+ const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
119
+ const doctorScope = scopeParts.join("/");
120
+ const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
121
+ const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" : "task";
122
+ const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
123
+ if (report.fixesApplied.length > 0) {
124
+ ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
125
+ }
126
+ // Proactive health tracking — filter to current milestone to avoid
127
+ // cross-milestone stale errors inflating the escalation counter
128
+ const currentMilestoneId = s.currentUnit.id.split("/")[0];
129
+ const milestoneIssues = currentMilestoneId
130
+ ? report.issues.filter(i => i.unitId === currentMilestoneId ||
131
+ i.unitId.startsWith(`${currentMilestoneId}/`))
132
+ : report.issues;
133
+ const summary = summarizeDoctorIssues(milestoneIssues);
134
+ recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
135
+ // Check if we should escalate to LLM-assisted heal
136
+ if (summary.errors > 0) {
137
+ const unresolvedErrors = milestoneIssues
138
+ .filter(i => i.severity === "error" && !i.fixable)
139
+ .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
140
+ const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
141
+ if (escalation.shouldEscalate) {
142
+ ctx.ui.notify(`Doctor heal escalation: ${escalation.reason}. Dispatching LLM-assisted heal.`, "warning");
143
+ try {
144
+ const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
145
+ const { dispatchDoctorHeal } = await import("./commands-handlers.js");
146
+ const actionable = report.issues.filter(i => i.severity === "error");
147
+ const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
148
+ const structuredIssues = formatDoctorIssuesForPrompt(actionable);
149
+ dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
150
+ return "dispatched";
151
+ }
152
+ catch (e) {
153
+ debugLog("postUnit", { phase: "doctor-heal-dispatch", error: String(e) });
154
+ }
155
+ }
156
+ }
157
+ }
158
+ catch (e) {
159
+ debugLog("postUnit", { phase: "doctor", error: String(e) });
141
160
  }
142
- catch {
143
- // Non-fatal
161
+ // Throttled STATE.md rebuild (skipped for lightweight sidecars)
162
+ if (!opts?.skipStateRebuild) {
163
+ const now = Date.now();
164
+ if (now - s.lastStateRebuildAt >= STATE_REBUILD_MIN_INTERVAL_MS) {
165
+ try {
166
+ await rebuildState(s.basePath);
167
+ s.lastStateRebuildAt = now;
168
+ autoCommitCurrentBranch(s.basePath, "state-rebuild", s.currentUnit.id);
169
+ }
170
+ catch (e) {
171
+ debugLog("postUnit", { phase: "state-rebuild", error: String(e) });
172
+ }
144
173
  }
145
174
  }
146
175
  // Prune dead bg-shell processes
@@ -148,27 +177,41 @@ export async function postUnitPreVerification(pctx) {
148
177
  const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
149
178
  pruneDeadProcesses();
150
179
  }
151
- catch {
152
- // Non-fatal
180
+ catch (e) {
181
+ debugLog("postUnit", { phase: "prune-bg-shell", error: String(e) });
153
182
  }
154
- // Sync worktree state back to project root
155
- if (s.originalBasePath && s.originalBasePath !== s.basePath) {
183
+ // Sync worktree state back to project root (skipped for lightweight sidecars)
184
+ if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
156
185
  try {
157
186
  syncStateToProjectRoot(s.basePath, s.originalBasePath, s.currentMilestoneId);
158
187
  }
159
- catch {
160
- // Non-fatal
188
+ catch (e) {
189
+ debugLog("postUnit", { phase: "worktree-sync", error: String(e) });
161
190
  }
162
191
  }
163
192
  // Rewrite-docs completion
164
193
  if (s.currentUnit.type === "rewrite-docs") {
165
194
  try {
166
195
  await resolveAllOverrides(s.basePath);
167
- resetRewriteCircuitBreaker();
196
+ s.rewriteAttemptCount = 0;
168
197
  ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
169
198
  }
170
- catch {
171
- // Non-fatal
199
+ catch (e) {
200
+ debugLog("postUnit", { phase: "rewrite-docs-resolve", error: String(e) });
201
+ }
202
+ }
203
+ // Reactive state cleanup on slice completion
204
+ if (s.currentUnit.type === "complete-slice") {
205
+ try {
206
+ const parts = s.currentUnit.id.split("/");
207
+ const [mid, sid] = parts;
208
+ if (mid && sid) {
209
+ const { clearReactiveState } = await import("./reactive-graph.js");
210
+ clearReactiveState(s.basePath, mid, sid);
211
+ }
212
+ }
213
+ catch (e) {
214
+ debugLog("postUnit", { phase: "reactive-state-cleanup", error: String(e) });
172
215
  }
173
216
  }
174
217
  // Post-triage: execute actionable resolutions
@@ -210,8 +253,8 @@ export async function postUnitPreVerification(pctx) {
210
253
  invalidateAllCaches();
211
254
  }
212
255
  }
213
- catch {
214
- // Non-fatal
256
+ catch (e) {
257
+ debugLog("postUnit", { phase: "artifact-verify", error: String(e) });
215
258
  }
216
259
  }
217
260
  else {
@@ -224,8 +267,8 @@ export async function postUnitPreVerification(pctx) {
224
267
  });
225
268
  clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
226
269
  }
227
- catch {
228
- // Non-fatal
270
+ catch (e) {
271
+ debugLog("postUnit", { phase: "hook-finalize", error: String(e) });
229
272
  }
230
273
  }
231
274
  }
@@ -338,8 +381,8 @@ export async function postUnitPostVerification(pctx) {
338
381
  }
339
382
  }
340
383
  }
341
- catch {
342
- // Triage check failure is non-fatal
384
+ catch (e) {
385
+ debugLog("postUnit", { phase: "triage-check", error: String(e) });
343
386
  }
344
387
  }
345
388
  // ── Quick-task dispatch ──
@@ -373,8 +416,8 @@ export async function postUnitPostVerification(pctx) {
373
416
  ctx.ui.notify(`Executing quick-task: ${capture.id} — "${capture.text}"`, "info");
374
417
  return "continue";
375
418
  }
376
- catch {
377
- // Non-fatal proceed to normal dispatch
419
+ catch (e) {
420
+ debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
378
421
  }
379
422
  }
380
423
  // Step mode → show wizard instead of dispatch
@@ -11,11 +11,15 @@ import { resolveMilestoneFile, resolveSliceFile, resolveSlicePath, resolveTasksD
11
11
  import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferences } from "./preferences.js";
12
12
  import { join } from "node:path";
13
13
  import { existsSync } from "node:fs";
14
- import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
15
- import { compressToTarget } from "./prompt-compressor.js";
16
- import { distillSummaries } from "./summary-distiller.js";
14
+ import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js";
17
15
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
18
- import { chunkByRelevance, formatChunks } from "./semantic-chunker.js";
16
+ // ─── Preamble Cap ─────────────────────────────────────────────────────────────
17
+ const MAX_PREAMBLE_CHARS = 30_000;
18
+ function capPreamble(preamble) {
19
+ if (preamble.length <= MAX_PREAMBLE_CHARS)
20
+ return preamble;
21
+ return truncateAtSectionBoundary(preamble, MAX_PREAMBLE_CHARS).content;
22
+ }
19
23
  // ─── Executor Constraints ─────────────────────────────────────────────────────
20
24
  /**
21
25
  * Format executor context constraints for injection into the plan-slice prompt.
@@ -126,14 +130,9 @@ export async function inlineFileSmart(absPath, relPath, label, query, threshold
126
130
  if (content.length <= threshold || !query) {
127
131
  return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
128
132
  }
129
- // Use semantic chunking for large files
130
- const result = chunkByRelevance(content, query, { maxChunks: 5, minScore: 0.05 });
131
- // If chunking didn't save much (< 20%), just include full content
132
- if (result.savingsPercent < 20) {
133
- return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
134
- }
135
- const formatted = formatChunks(result, relPath);
136
- return `### ${label} (${result.omittedChunks} sections omitted for relevance)\nSource: \`${relPath}\`\n\n${formatted}`;
133
+ // For large files, truncate at section boundary
134
+ const truncated = truncateAtSectionBoundary(content, threshold).content;
135
+ return `### ${label}\nSource: \`${relPath}\`\n\n${truncated}`;
137
136
  }
138
137
  /**
139
138
  * Load and inline dependency slice summaries (full content, not just paths).
@@ -165,21 +164,6 @@ export async function inlineDependencySummaries(mid, sid, base, budgetChars) {
165
164
  }
166
165
  const result = sections.join("\n\n");
167
166
  if (budgetChars !== undefined && result.length > budgetChars) {
168
- // For 3+ summaries, try distillation first (preserves more information)
169
- if (sections.length >= 3) {
170
- const rawSummaries = sections.map(s => {
171
- // Extract content after the header line
172
- const lines = s.split("\n");
173
- const contentStart = lines.findIndex(l => l.startsWith("Source:"));
174
- return contentStart >= 0 ? lines.slice(contentStart + 1).join("\n").trim() : s;
175
- });
176
- const distilled = distillSummaries(rawSummaries, budgetChars);
177
- if (distilled.content.length <= budgetChars) {
178
- return distilled.content;
179
- }
180
- }
181
- // Fall back to section-boundary truncation
182
- const { truncateAtSectionBoundary } = await import("./context-budget.js");
183
167
  return truncateAtSectionBoundary(result, budgetChars).content;
184
168
  }
185
169
  return result;
@@ -414,6 +398,35 @@ export async function getPriorTaskSummaryPaths(mid, sid, currentTid, base) {
414
398
  })
415
399
  .map(f => `${sRel}/tasks/${f}`);
416
400
  }
401
+ /**
402
+ * Get carry-forward summary paths scoped to a task's derived dependencies.
403
+ *
404
+ * Instead of all prior tasks (order-based), returns only summaries for task
405
+ * IDs in `dependsOn`. Used by reactive-execute to give each subagent only
406
+ * the context it actually needs — not sibling tasks from a parallel batch.
407
+ *
408
+ * Falls back to order-based when dependsOn is empty (root tasks still get
409
+ * any available prior summaries for continuity).
410
+ */
411
+ export async function getDependencyTaskSummaryPaths(mid, sid, currentTid, dependsOn, base) {
412
+ // If no dependencies, fall back to order-based for root tasks
413
+ if (dependsOn.length === 0) {
414
+ return getPriorTaskSummaryPaths(mid, sid, currentTid, base);
415
+ }
416
+ const tDir = resolveTasksDir(base, mid, sid);
417
+ if (!tDir)
418
+ return [];
419
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
420
+ const sRel = relSlicePath(base, mid, sid);
421
+ const depSet = new Set(dependsOn.map((d) => d.toUpperCase()));
422
+ return summaryFiles
423
+ .filter((f) => {
424
+ // Extract task ID from filename: "T02-SUMMARY.md" → "T02"
425
+ const tid = f.replace(/-SUMMARY\.md$/i, "").toUpperCase();
426
+ return depSet.has(tid);
427
+ })
428
+ .map((f) => `${sRel}/tasks/${f}`);
429
+ }
417
430
  // ─── Adaptive Replanning Checks ────────────────────────────────────────────
418
431
  /**
419
432
  * Check if the most recently completed slice needs reassessment.
@@ -517,7 +530,7 @@ export async function buildResearchMilestonePrompt(mid, midTitle, base) {
517
530
  if (knowledgeInlineRM)
518
531
  inlined.push(knowledgeInlineRM);
519
532
  inlined.push(inlineTemplate("research", "Research"));
520
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
533
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
521
534
  const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
522
535
  return loadPrompt("research-milestone", {
523
536
  workingDirectory: base,
@@ -570,7 +583,7 @@ export async function buildPlanMilestonePrompt(mid, midTitle, base, level) {
570
583
  inlined.push(inlineTemplate("plan", "Slice Plan"));
571
584
  inlined.push(inlineTemplate("task-plan", "Task Plan"));
572
585
  }
573
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
586
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
574
587
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
575
588
  const researchOutputPath = join(base, relMilestoneFile(base, mid, "RESEARCH"));
576
589
  const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
@@ -618,7 +631,7 @@ export async function buildResearchSlicePrompt(mid, _midTitle, sid, sTitle, base
618
631
  const overridesInline = formatOverridesSection(activeOverrides);
619
632
  if (overridesInline)
620
633
  inlined.unshift(overridesInline);
621
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
634
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
622
635
  const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
623
636
  return loadPrompt("research-slice", {
624
637
  workingDirectory: base,
@@ -664,7 +677,7 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
664
677
  const planOverridesInline = formatOverridesSection(planActiveOverrides);
665
678
  if (planOverridesInline)
666
679
  inlined.unshift(planOverridesInline);
667
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
680
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
668
681
  // Build executor context constraints from the budget engine
669
682
  const executorContextConstraints = formatExecutorConstraints();
670
683
  const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
@@ -688,8 +701,11 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
688
701
  });
689
702
  }
690
703
  export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, level) {
691
- const inlineLevel = level ?? resolveInlineLevel();
692
- const priorSummaries = await getPriorTaskSummaryPaths(mid, sid, tid, base);
704
+ const opts = typeof level === "object" && level !== null && !Array.isArray(level)
705
+ ? level
706
+ : { level: level };
707
+ const inlineLevel = opts.level ?? resolveInlineLevel();
708
+ const priorSummaries = opts.carryForwardPaths ?? await getPriorTaskSummaryPaths(mid, sid, tid, base);
693
709
  const priorLines = priorSummaries.length > 0
694
710
  ? priorSummaries.map(p => `- \`${p}\``).join("\n")
695
711
  : "- (no prior tasks)";
@@ -745,15 +761,11 @@ export async function buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base
745
761
  const contextWindow = resolveExecutorContextWindow(undefined, prefs?.preferences);
746
762
  const budgets = computeBudgets(contextWindow);
747
763
  const verificationBudget = `~${Math.round(budgets.verificationBudgetChars / 1000)}K chars`;
748
- // Compress carry-forward section when it exceeds 40% of inline context budget.
749
- // Only compress when compression_strategy is "compress" (budget/balanced profiles).
764
+ // Truncate carry-forward section when it exceeds 40% of inline context budget.
750
765
  const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
751
766
  let finalCarryForward = carryForwardSection;
752
767
  if (carryForwardSection.length > carryForwardBudget) {
753
- const { resolveCompressionStrategy } = await import("./preferences.js");
754
- if (resolveCompressionStrategy() === "compress") {
755
- finalCarryForward = compressToTarget(carryForwardSection, carryForwardBudget).content;
756
- }
768
+ finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
757
769
  }
758
770
  return loadPrompt("execute-task", {
759
771
  overridesSection,
@@ -811,7 +823,7 @@ export async function buildCompleteSlicePrompt(mid, _midTitle, sid, sTitle, base
811
823
  const completeOverridesInline = formatOverridesSection(completeActiveOverrides);
812
824
  if (completeOverridesInline)
813
825
  inlined.unshift(completeOverridesInline);
814
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
826
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
815
827
  const sliceRel = relSlicePath(base, mid, sid);
816
828
  const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
817
829
  const sliceUatPath = join(base, `${sliceRel}/${sid}-UAT.md`);
@@ -867,7 +879,7 @@ export async function buildCompleteMilestonePrompt(mid, midTitle, base, level) {
867
879
  if (contextInline)
868
880
  inlined.push(contextInline);
869
881
  inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
870
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
882
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
871
883
  const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
872
884
  return loadPrompt("complete-milestone", {
873
885
  workingDirectory: base,
@@ -934,7 +946,7 @@ export async function buildValidateMilestonePrompt(mid, midTitle, base, level) {
934
946
  const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
935
947
  if (contextInline)
936
948
  inlined.push(contextInline);
937
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
949
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
938
950
  const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
939
951
  const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
940
952
  return loadPrompt("validate-milestone", {
@@ -982,7 +994,7 @@ export async function buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base) {
982
994
  const replanOverridesInline = formatOverridesSection(replanActiveOverrides);
983
995
  if (replanOverridesInline)
984
996
  inlined.unshift(replanOverridesInline);
985
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
997
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
986
998
  const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
987
999
  // Build capture context for replan prompt (captures that triggered this replan)
988
1000
  let captureContext = "(none)";
@@ -1022,7 +1034,7 @@ export async function buildRunUatPrompt(mid, sliceId, uatPath, uatContent, base)
1022
1034
  const projectInline = await inlineProjectFromDb(base);
1023
1035
  if (projectInline)
1024
1036
  inlined.push(projectInline);
1025
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1037
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1026
1038
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
1027
1039
  const uatType = extractUatType(uatContent) ?? "human-experience";
1028
1040
  return loadPrompt("run-uat", {
@@ -1058,7 +1070,7 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
1058
1070
  const knowledgeInlineRA = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
1059
1071
  if (knowledgeInlineRA)
1060
1072
  inlined.push(knowledgeInlineRA);
1061
- const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1073
+ const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1062
1074
  const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
1063
1075
  // Build deferred captures context for reassess prompt
1064
1076
  let deferredCaptures = "(none)";
@@ -1090,6 +1102,63 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
1090
1102
  commitInstruction: reassessCommitInstruction,
1091
1103
  });
1092
1104
  }
1105
+ // ─── Reactive Execute Prompt ──────────────────────────────────────────────
1106
+ export async function buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, readyTaskIds, base) {
1107
+ const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
1108
+ // Build graph for context
1109
+ const taskIO = await loadSliceTaskIO(base, mid, sid);
1110
+ const graph = deriveTaskGraph(taskIO);
1111
+ const metrics = graphMetrics(graph);
1112
+ // Build graph context section
1113
+ const graphLines = [];
1114
+ for (const node of graph) {
1115
+ const status = node.done ? "✅ done" : readyTaskIds.includes(node.id) ? "🟢 ready" : "⏳ waiting";
1116
+ const deps = node.dependsOn.length > 0 ? ` (depends on: ${node.dependsOn.join(", ")})` : "";
1117
+ graphLines.push(`- **${node.id}: ${node.title}** — ${status}${deps}`);
1118
+ if (node.outputFiles.length > 0) {
1119
+ graphLines.push(` - Outputs: ${node.outputFiles.map(f => `\`${f}\``).join(", ")}`);
1120
+ }
1121
+ }
1122
+ const graphContext = [
1123
+ `Tasks: ${metrics.taskCount}, Edges: ${metrics.edgeCount}, Ready: ${metrics.readySetSize}`,
1124
+ "",
1125
+ ...graphLines,
1126
+ ].join("\n");
1127
+ // Build individual subagent prompts for each ready task
1128
+ const subagentSections = [];
1129
+ const readyTaskListLines = [];
1130
+ for (const tid of readyTaskIds) {
1131
+ const node = graph.find((n) => n.id === tid);
1132
+ const tTitle = node?.title ?? tid;
1133
+ readyTaskListLines.push(`- **${tid}: ${tTitle}**`);
1134
+ // Build dependency-scoped carry-forward paths for this task
1135
+ const depPaths = await getDependencyTaskSummaryPaths(mid, sid, tid, node?.dependsOn ?? [], base);
1136
+ // Build a full execute-task prompt with dependency-based carry-forward
1137
+ const taskPrompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, { carryForwardPaths: depPaths });
1138
+ subagentSections.push([
1139
+ `### ${tid}: ${tTitle}`,
1140
+ "",
1141
+ "Use this as the prompt for a `subagent` call:",
1142
+ "",
1143
+ "```",
1144
+ taskPrompt,
1145
+ "```",
1146
+ ].join("\n"));
1147
+ }
1148
+ const inlinedTemplates = inlineTemplate("task-summary", "Task Summary");
1149
+ return loadPrompt("reactive-execute", {
1150
+ workingDirectory: base,
1151
+ milestoneId: mid,
1152
+ milestoneTitle: midTitle,
1153
+ sliceId: sid,
1154
+ sliceTitle: sTitle,
1155
+ graphContext,
1156
+ readyTaskCount: String(readyTaskIds.length),
1157
+ readyTaskList: readyTaskListLines.join("\n"),
1158
+ subagentPrompts: subagentSections.join("\n\n---\n\n"),
1159
+ inlinedTemplates,
1160
+ });
1161
+ }
1093
1162
  export async function buildRewriteDocsPrompt(mid, midTitle, activeSlice, base, overrides) {
1094
1163
  const sid = activeSlice?.id;
1095
1164
  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,39 @@ 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 each dispatched task's summary exists.
112
+ // The unitId encodes the batch: "{mid}/{sid}/reactive+T02,T03"
113
+ if (unitType === "reactive-execute") {
114
+ const parts = unitId.split("/");
115
+ const mid = parts[0];
116
+ const sidAndBatch = parts[1];
117
+ const batchPart = parts[2]; // "reactive+T02,T03"
118
+ if (!mid || !sidAndBatch || !batchPart)
119
+ return false;
120
+ const sid = sidAndBatch;
121
+ const plusIdx = batchPart.indexOf("+");
122
+ if (plusIdx === -1) {
123
+ // Legacy format "reactive" without batch IDs — fall back to "any summary"
124
+ const tDir = resolveTasksDir(base, mid, sid);
125
+ if (!tDir)
126
+ return false;
127
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
128
+ return summaryFiles.length > 0;
129
+ }
130
+ const batchIds = batchPart.slice(plusIdx + 1).split(",").filter(Boolean);
131
+ if (batchIds.length === 0)
132
+ return false;
133
+ const tDir = resolveTasksDir(base, mid, sid);
134
+ if (!tDir)
135
+ return false;
136
+ const existingSummaries = new Set(resolveTaskFiles(tDir, "SUMMARY").map((f) => f.replace(/-SUMMARY\.md$/i, "").toUpperCase()));
137
+ // Every dispatched task must have a summary file
138
+ for (const tid of batchIds) {
139
+ if (!existingSummaries.has(tid.toUpperCase()))
140
+ return false;
141
+ }
142
+ return true;
143
+ }
108
144
  const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
109
145
  // For unit types with no verifiable artifact (null path), the parent directory
110
146
  // is missing on disk — treat as stale completion state so the key gets evicted (#313).
@@ -11,7 +11,7 @@
11
11
  import { deriveState } from "./state.js";
12
12
  import { loadFile, getManifestStatus } from "./files.js";
13
13
  import { loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode, } from "./preferences.js";
14
- import { ensureGsdSymlink } from "./repo-identity.js";
14
+ import { ensureGsdSymlink, validateProjectId } from "./repo-identity.js";
15
15
  import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
16
16
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
17
17
  import { gsdRoot, resolveMilestoneFile } from "./paths.js";
@@ -63,6 +63,12 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
63
63
  return false;
64
64
  }
65
65
  try {
66
+ // Validate GSD_PROJECT_ID early so the user gets immediate feedback
67
+ const customProjectId = process.env.GSD_PROJECT_ID;
68
+ if (customProjectId && !validateProjectId(customProjectId)) {
69
+ ctx.ui.notify(`GSD_PROJECT_ID must contain only alphanumeric characters, hyphens, and underscores. Got: "${customProjectId}"`, "error");
70
+ return releaseLockAndReturn();
71
+ }
66
72
  // Ensure git repo exists
67
73
  if (!nativeIsRepo(base)) {
68
74
  const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
@@ -303,11 +309,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
303
309
  // ── Auto-worktree setup ──
304
310
  s.originalBasePath = base;
305
311
  const isUnderGsdWorktrees = (p) => {
312
+ // Direct layout: /.gsd/worktrees/
306
313
  const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
307
314
  if (p.includes(marker))
308
315
  return true;
309
316
  const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
310
- return p.endsWith(worktreesSuffix);
317
+ if (p.endsWith(worktreesSuffix))
318
+ return true;
319
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
320
+ const symlinkRe = new RegExp(`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees(?:\\${pathSep}|$)`);
321
+ return symlinkRe.test(p);
311
322
  };
312
323
  if (s.currentMilestoneId &&
313
324
  shouldUseWorktreeIsolation() &&