gsd-pi 2.37.1 → 2.38.0-dev.63ad7e5

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 (201) 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 +6 -23
  16. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -9
  17. package/dist/resources/extensions/gsd/auto-loop.js +68 -97
  18. package/dist/resources/extensions/gsd/auto-post-unit.js +87 -69
  19. package/dist/resources/extensions/gsd/auto-prompts.js +98 -33
  20. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  21. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  23. package/dist/resources/extensions/gsd/auto.js +143 -96
  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-prefs-wizard.js +1 -1
  28. package/dist/resources/extensions/gsd/commands.js +22 -2
  29. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  30. package/dist/resources/extensions/gsd/detection.js +1 -2
  31. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  32. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  33. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  34. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  35. package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
  36. package/dist/resources/extensions/gsd/doctor.js +184 -11
  37. package/dist/resources/extensions/gsd/export.js +1 -1
  38. package/dist/resources/extensions/gsd/files.js +43 -2
  39. package/dist/resources/extensions/gsd/forensics.js +1 -1
  40. package/dist/resources/extensions/gsd/index.js +2 -1
  41. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  42. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  43. package/dist/resources/extensions/gsd/package.json +1 -1
  44. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  45. package/dist/resources/extensions/gsd/preferences-types.js +2 -2
  46. package/dist/resources/extensions/gsd/preferences-validation.js +43 -11
  47. package/dist/resources/extensions/gsd/preferences.js +5 -5
  48. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  49. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  50. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  51. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  52. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  53. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  54. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  55. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  56. package/dist/resources/extensions/gsd/prompts/run-uat.md +25 -10
  57. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  58. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  59. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  60. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  61. package/dist/resources/extensions/gsd/state.js +1 -1
  62. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  63. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  64. package/dist/resources/extensions/gsd/worktree.js +35 -16
  65. package/dist/resources/extensions/remote-questions/status.js +2 -1
  66. package/dist/resources/extensions/remote-questions/store.js +2 -1
  67. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  68. package/dist/resources/extensions/subagent/index.js +12 -3
  69. package/dist/resources/extensions/subagent/isolation.js +2 -1
  70. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  71. package/dist/resources/extensions/universal-config/package.json +1 -1
  72. package/dist/welcome-screen.d.ts +12 -0
  73. package/dist/welcome-screen.js +53 -0
  74. package/package.json +2 -1
  75. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  76. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  77. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  78. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/models.generated.js +172 -0
  80. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  82. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  83. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  84. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  85. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  86. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  87. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  88. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  89. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  90. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  91. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  92. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  93. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  95. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  96. package/packages/pi-ai/dist/types.d.ts +2 -2
  97. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  98. package/packages/pi-ai/dist/types.js.map +1 -1
  99. package/packages/pi-ai/package.json +1 -0
  100. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  101. package/packages/pi-ai/src/models.generated.ts +172 -0
  102. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  103. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  104. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  105. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  106. package/packages/pi-ai/src/types.ts +2 -0
  107. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  109. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  112. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  113. package/packages/pi-coding-agent/package.json +1 -1
  114. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  115. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  116. package/pkg/package.json +1 -1
  117. package/src/resources/extensions/cmux/index.ts +57 -1
  118. package/src/resources/extensions/env-utils.ts +31 -0
  119. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  120. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  121. package/src/resources/extensions/gsd/auto-dispatch.ts +99 -8
  122. package/src/resources/extensions/gsd/auto-loop.ts +88 -133
  123. package/src/resources/extensions/gsd/auto-post-unit.ts +64 -40
  124. package/src/resources/extensions/gsd/auto-prompts.ts +132 -36
  125. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  126. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  127. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  128. package/src/resources/extensions/gsd/auto.ts +139 -101
  129. package/src/resources/extensions/gsd/captures.ts +10 -1
  130. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  131. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  132. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  133. package/src/resources/extensions/gsd/commands.ts +24 -2
  134. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  135. package/src/resources/extensions/gsd/detection.ts +2 -2
  136. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  137. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  138. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  139. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  140. package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
  141. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  142. package/src/resources/extensions/gsd/doctor.ts +177 -13
  143. package/src/resources/extensions/gsd/export.ts +1 -1
  144. package/src/resources/extensions/gsd/files.ts +47 -2
  145. package/src/resources/extensions/gsd/forensics.ts +1 -1
  146. package/src/resources/extensions/gsd/index.ts +3 -1
  147. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  148. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  149. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  150. package/src/resources/extensions/gsd/preferences-types.ts +5 -5
  151. package/src/resources/extensions/gsd/preferences-validation.ts +42 -11
  152. package/src/resources/extensions/gsd/preferences.ts +5 -5
  153. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  154. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  155. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  156. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  157. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  158. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  159. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  160. package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  161. package/src/resources/extensions/gsd/prompts/run-uat.md +25 -10
  162. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  163. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  164. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  165. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  166. package/src/resources/extensions/gsd/state.ts +1 -1
  167. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  168. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  169. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +11 -31
  170. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  171. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  172. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
  173. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  174. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  175. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  176. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  177. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  178. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  179. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  180. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  181. package/src/resources/extensions/gsd/types.ts +43 -1
  182. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  183. package/src/resources/extensions/gsd/worktree.ts +35 -15
  184. package/src/resources/extensions/remote-questions/status.ts +3 -1
  185. package/src/resources/extensions/remote-questions/store.ts +3 -1
  186. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  187. package/src/resources/extensions/subagent/index.ts +12 -3
  188. package/src/resources/extensions/subagent/isolation.ts +3 -1
  189. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  190. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  191. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  192. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  193. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  194. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  195. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  196. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  197. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  198. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  199. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  200. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  201. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -21,10 +21,7 @@ import type { GSDPreferences } from "./preferences.js";
21
21
  import { join } from "node:path";
22
22
  import { existsSync } from "node:fs";
23
23
  import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
24
- import { compressToTarget } from "./prompt-compressor.js";
25
- import { distillSummaries } from "./summary-distiller.js";
26
24
  import { formatDecisionsCompact, formatRequirementsCompact } from "./structured-data-formatter.js";
27
- import { chunkByRelevance, formatChunks } from "./semantic-chunker.js";
28
25
 
29
26
  // ─── Executor Constraints ─────────────────────────────────────────────────────
30
27
 
@@ -159,16 +156,10 @@ export async function inlineFileSmart(
159
156
  return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
160
157
  }
161
158
 
162
- // Use semantic chunking for large files
163
- const result = chunkByRelevance(content, query, { maxChunks: 5, minScore: 0.05 });
164
-
165
- // If chunking didn't save much (< 20%), just include full content
166
- if (result.savingsPercent < 20) {
167
- return `### ${label}\nSource: \`${relPath}\`\n\n${content.trim()}`;
168
- }
169
-
170
- const formatted = formatChunks(result, relPath);
171
- return `### ${label} (${result.omittedChunks} sections omitted for relevance)\nSource: \`${relPath}\`\n\n${formatted}`;
159
+ // For large files, truncate at section boundary
160
+ const { truncateAtSectionBoundary } = await import("./context-budget.js");
161
+ const truncated = truncateAtSectionBoundary(content, threshold).content;
162
+ return `### ${label}\nSource: \`${relPath}\`\n\n${truncated}`;
172
163
  }
173
164
 
174
165
  /**
@@ -202,20 +193,6 @@ export async function inlineDependencySummaries(
202
193
 
203
194
  const result = sections.join("\n\n");
204
195
  if (budgetChars !== undefined && result.length > budgetChars) {
205
- // For 3+ summaries, try distillation first (preserves more information)
206
- if (sections.length >= 3) {
207
- const rawSummaries = sections.map(s => {
208
- // Extract content after the header line
209
- const lines = s.split("\n");
210
- const contentStart = lines.findIndex(l => l.startsWith("Source:"));
211
- return contentStart >= 0 ? lines.slice(contentStart + 1).join("\n").trim() : s;
212
- });
213
- const distilled = distillSummaries(rawSummaries, budgetChars);
214
- if (distilled.content.length <= budgetChars) {
215
- return distilled.content;
216
- }
217
- }
218
- // Fall back to section-boundary truncation
219
196
  const { truncateAtSectionBoundary } = await import("./context-budget.js");
220
197
  return truncateAtSectionBoundary(result, budgetChars).content;
221
198
  }
@@ -485,6 +462,41 @@ export async function getPriorTaskSummaryPaths(
485
462
  .map(f => `${sRel}/tasks/${f}`);
486
463
  }
487
464
 
465
+ /**
466
+ * Get carry-forward summary paths scoped to a task's derived dependencies.
467
+ *
468
+ * Instead of all prior tasks (order-based), returns only summaries for task
469
+ * IDs in `dependsOn`. Used by reactive-execute to give each subagent only
470
+ * the context it actually needs — not sibling tasks from a parallel batch.
471
+ *
472
+ * Falls back to order-based when dependsOn is empty (root tasks still get
473
+ * any available prior summaries for continuity).
474
+ */
475
+ export async function getDependencyTaskSummaryPaths(
476
+ mid: string, sid: string, currentTid: string,
477
+ dependsOn: string[], base: string,
478
+ ): Promise<string[]> {
479
+ // If no dependencies, fall back to order-based for root tasks
480
+ if (dependsOn.length === 0) {
481
+ return getPriorTaskSummaryPaths(mid, sid, currentTid, base);
482
+ }
483
+
484
+ const tDir = resolveTasksDir(base, mid, sid);
485
+ if (!tDir) return [];
486
+
487
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
488
+ const sRel = relSlicePath(base, mid, sid);
489
+ const depSet = new Set(dependsOn.map((d) => d.toUpperCase()));
490
+
491
+ return summaryFiles
492
+ .filter((f) => {
493
+ // Extract task ID from filename: "T02-SUMMARY.md" → "T02"
494
+ const tid = f.replace(/-SUMMARY\.md$/i, "").toUpperCase();
495
+ return depSet.has(tid);
496
+ })
497
+ .map((f) => `${sRel}/tasks/${f}`);
498
+ }
499
+
488
500
  // ─── Adaptive Replanning Checks ────────────────────────────────────────────
489
501
 
490
502
  /**
@@ -772,13 +784,24 @@ export async function buildPlanSlicePrompt(
772
784
  });
773
785
  }
774
786
 
787
+ /** Options for customizing execute-task prompt construction. */
788
+ export interface ExecuteTaskPromptOptions {
789
+ level?: InlineLevel;
790
+ /** Override carry-forward paths (dependency-based instead of order-based). */
791
+ carryForwardPaths?: string[];
792
+ }
793
+
775
794
  export async function buildExecuteTaskPrompt(
776
795
  mid: string, sid: string, sTitle: string,
777
- tid: string, tTitle: string, base: string, level?: InlineLevel,
796
+ tid: string, tTitle: string, base: string,
797
+ level?: InlineLevel | ExecuteTaskPromptOptions,
778
798
  ): Promise<string> {
779
- const inlineLevel = level ?? resolveInlineLevel();
799
+ const opts: ExecuteTaskPromptOptions = typeof level === "object" && level !== null && !Array.isArray(level)
800
+ ? level
801
+ : { level: level as InlineLevel | undefined };
802
+ const inlineLevel = opts.level ?? resolveInlineLevel();
780
803
 
781
- const priorSummaries = await getPriorTaskSummaryPaths(mid, sid, tid, base);
804
+ const priorSummaries = opts.carryForwardPaths ?? await getPriorTaskSummaryPaths(mid, sid, tid, base);
782
805
  const priorLines = priorSummaries.length > 0
783
806
  ? priorSummaries.map(p => `- \`${p}\``).join("\n")
784
807
  : "- (no prior tasks)";
@@ -854,15 +877,12 @@ export async function buildExecuteTaskPrompt(
854
877
  const budgets = computeBudgets(contextWindow);
855
878
  const verificationBudget = `~${Math.round(budgets.verificationBudgetChars / 1000)}K chars`;
856
879
 
857
- // Compress carry-forward section when it exceeds 40% of inline context budget.
858
- // Only compress when compression_strategy is "compress" (budget/balanced profiles).
880
+ // Truncate carry-forward section when it exceeds 40% of inline context budget.
859
881
  const carryForwardBudget = Math.floor(budgets.inlineContextBudgetChars * 0.4);
860
882
  let finalCarryForward = carryForwardSection;
861
883
  if (carryForwardSection.length > carryForwardBudget) {
862
- const { resolveCompressionStrategy } = await import("./preferences.js");
863
- if (resolveCompressionStrategy() === "compress") {
864
- finalCarryForward = compressToTarget(carryForwardSection, carryForwardBudget).content;
865
- }
884
+ const { truncateAtSectionBoundary } = await import("./context-budget.js");
885
+ finalCarryForward = truncateAtSectionBoundary(carryForwardSection, carryForwardBudget).content;
866
886
  }
867
887
 
868
888
  return loadPrompt("execute-task", {
@@ -1234,6 +1254,82 @@ export async function buildReassessRoadmapPrompt(
1234
1254
  });
1235
1255
  }
1236
1256
 
1257
+ // ─── Reactive Execute Prompt ──────────────────────────────────────────────
1258
+
1259
+ export async function buildReactiveExecutePrompt(
1260
+ mid: string, midTitle: string, sid: string, sTitle: string,
1261
+ readyTaskIds: string[], base: string,
1262
+ ): Promise<string> {
1263
+ const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
1264
+
1265
+ // Build graph for context
1266
+ const taskIO = await loadSliceTaskIO(base, mid, sid);
1267
+ const graph = deriveTaskGraph(taskIO);
1268
+ const metrics = graphMetrics(graph);
1269
+
1270
+ // Build graph context section
1271
+ const graphLines: string[] = [];
1272
+ for (const node of graph) {
1273
+ const status = node.done ? "✅ done" : readyTaskIds.includes(node.id) ? "🟢 ready" : "⏳ waiting";
1274
+ const deps = node.dependsOn.length > 0 ? ` (depends on: ${node.dependsOn.join(", ")})` : "";
1275
+ graphLines.push(`- **${node.id}: ${node.title}** — ${status}${deps}`);
1276
+ if (node.outputFiles.length > 0) {
1277
+ graphLines.push(` - Outputs: ${node.outputFiles.map(f => `\`${f}\``).join(", ")}`);
1278
+ }
1279
+ }
1280
+ const graphContext = [
1281
+ `Tasks: ${metrics.taskCount}, Edges: ${metrics.edgeCount}, Ready: ${metrics.readySetSize}`,
1282
+ "",
1283
+ ...graphLines,
1284
+ ].join("\n");
1285
+
1286
+ // Build individual subagent prompts for each ready task
1287
+ const subagentSections: string[] = [];
1288
+ const readyTaskListLines: string[] = [];
1289
+
1290
+ for (const tid of readyTaskIds) {
1291
+ const node = graph.find((n) => n.id === tid);
1292
+ const tTitle = node?.title ?? tid;
1293
+ readyTaskListLines.push(`- **${tid}: ${tTitle}**`);
1294
+
1295
+ // Build dependency-scoped carry-forward paths for this task
1296
+ const depPaths = await getDependencyTaskSummaryPaths(
1297
+ mid, sid, tid, node?.dependsOn ?? [], base,
1298
+ );
1299
+
1300
+ // Build a full execute-task prompt with dependency-based carry-forward
1301
+ const taskPrompt = await buildExecuteTaskPrompt(
1302
+ mid, sid, sTitle, tid, tTitle, base,
1303
+ { carryForwardPaths: depPaths },
1304
+ );
1305
+
1306
+ subagentSections.push([
1307
+ `### ${tid}: ${tTitle}`,
1308
+ "",
1309
+ "Use this as the prompt for a `subagent` call:",
1310
+ "",
1311
+ "```",
1312
+ taskPrompt,
1313
+ "```",
1314
+ ].join("\n"));
1315
+ }
1316
+
1317
+ const inlinedTemplates = inlineTemplate("task-summary", "Task Summary");
1318
+
1319
+ return loadPrompt("reactive-execute", {
1320
+ workingDirectory: base,
1321
+ milestoneId: mid,
1322
+ milestoneTitle: midTitle,
1323
+ sliceId: sid,
1324
+ sliceTitle: sTitle,
1325
+ graphContext,
1326
+ readyTaskCount: String(readyTaskIds.length),
1327
+ readyTaskList: readyTaskListLines.join("\n"),
1328
+ subagentPrompts: subagentSections.join("\n\n---\n\n"),
1329
+ inlinedTemplates,
1330
+ });
1331
+ }
1332
+
1237
1333
  export async function buildRewriteDocsPrompt(
1238
1334
  mid: string, midTitle: string,
1239
1335
  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).
@@ -20,7 +20,7 @@ import {
20
20
  resolveSkillDiscoveryMode,
21
21
  getIsolationMode,
22
22
  } from "./preferences.js";
23
- import { ensureGsdSymlink } from "./repo-identity.js";
23
+ import { ensureGsdSymlink, validateProjectId } from "./repo-identity.js";
24
24
  import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
25
25
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
26
26
  import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
@@ -130,6 +130,16 @@ export async function bootstrapAutoSession(
130
130
  }
131
131
 
132
132
  try {
133
+ // Validate GSD_PROJECT_ID early so the user gets immediate feedback
134
+ const customProjectId = process.env.GSD_PROJECT_ID;
135
+ if (customProjectId && !validateProjectId(customProjectId)) {
136
+ ctx.ui.notify(
137
+ `GSD_PROJECT_ID must contain only alphanumeric characters, hyphens, and underscores. Got: "${customProjectId}"`,
138
+ "error",
139
+ );
140
+ return releaseLockAndReturn();
141
+ }
142
+
133
143
  // Ensure git repo exists
134
144
  if (!nativeIsRepo(base)) {
135
145
  const mainBranch =
@@ -429,10 +439,16 @@ export async function bootstrapAutoSession(
429
439
  s.originalBasePath = base;
430
440
 
431
441
  const isUnderGsdWorktrees = (p: string): boolean => {
442
+ // Direct layout: /.gsd/worktrees/
432
443
  const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
433
444
  if (p.includes(marker)) return true;
434
445
  const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
435
- return p.endsWith(worktreesSuffix);
446
+ if (p.endsWith(worktreesSuffix)) return true;
447
+ // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
448
+ const symlinkRe = new RegExp(
449
+ `\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees(?:\\${pathSep}|$)`,
450
+ );
451
+ return symlinkRe.test(p);
436
452
  };
437
453
 
438
454
  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);
@@ -536,129 +536,167 @@ export async function stopAuto(
536
536
  if (!s.active && !s.paused) return;
537
537
  const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
538
538
  const reasonSuffix = reason ? ` — ${reason}` : "";
539
- clearUnitTimeout();
540
- if (lockBase()) clearLock(lockBase());
541
- if (lockBase()) releaseSessionLock(lockBase());
542
- clearSkillSnapshot();
543
- resetSkillTelemetry();
544
539
 
545
- // Remove SIGTERM handler registered at auto-mode start
546
- deregisterSigtermHandler();
540
+ try {
541
+ // ── Step 1: Timers and locks ──
542
+ try {
543
+ clearUnitTimeout();
544
+ if (lockBase()) clearLock(lockBase());
545
+ if (lockBase()) releaseSessionLock(lockBase());
546
+ } catch (e) {
547
+ debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
548
+ }
547
549
 
548
- // ── Auto-worktree: exit worktree and reset s.basePath on stop ──
549
- if (s.currentMilestoneId) {
550
- const notifyCtx = ctx
551
- ? { notify: ctx.ui.notify.bind(ctx.ui) }
552
- : { notify: () => {} };
553
- buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
554
- preserveBranch: true,
555
- });
556
- }
550
+ // ── Step 2: Skill state ──
551
+ try {
552
+ clearSkillSnapshot();
553
+ resetSkillTelemetry();
554
+ } catch (e) {
555
+ debugLog("stop-cleanup-skills", { error: e instanceof Error ? e.message : String(e) });
556
+ }
557
557
 
558
- // ── DB cleanup: close the SQLite connection ──
559
- if (isDbAvailable()) {
558
+ // ── Step 3: SIGTERM handler ──
560
559
  try {
561
- const { closeDatabase } = await import("./gsd-db.js");
562
- closeDatabase();
560
+ deregisterSigtermHandler();
563
561
  } catch (e) {
564
- debugLog("db-close-failed", {
565
- error: e instanceof Error ? e.message : String(e),
566
- });
562
+ debugLog("stop-cleanup-sigterm", { error: e instanceof Error ? e.message : String(e) });
567
563
  }
568
- }
569
564
 
570
- if (s.originalBasePath) {
571
- s.basePath = s.originalBasePath;
565
+ // ── Step 4: Auto-worktree exit ──
572
566
  try {
573
- process.chdir(s.basePath);
574
- } catch {
575
- /* best-effort */
567
+ if (s.currentMilestoneId) {
568
+ const notifyCtx = ctx
569
+ ? { notify: ctx.ui.notify.bind(ctx.ui) }
570
+ : { notify: () => {} };
571
+ buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
572
+ preserveBranch: true,
573
+ });
574
+ }
575
+ } catch (e) {
576
+ debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
576
577
  }
577
- }
578
578
 
579
- const ledger = getLedger();
580
- if (ledger && ledger.units.length > 0) {
581
- const totals = getProjectTotals(ledger.units);
582
- ctx?.ui.notify(
583
- `Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
584
- "info",
585
- );
586
- } else {
587
- ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
588
- }
579
+ // ── Step 5: DB cleanup ──
580
+ if (isDbAvailable()) {
581
+ try {
582
+ const { closeDatabase } = await import("./gsd-db.js");
583
+ closeDatabase();
584
+ } catch (e) {
585
+ debugLog("db-close-failed", {
586
+ error: e instanceof Error ? e.message : String(e),
587
+ });
588
+ }
589
+ }
589
590
 
590
- if (s.basePath) {
591
+ // ── Step 6: Restore basePath and chdir ──
591
592
  try {
592
- await rebuildState(s.basePath);
593
+ if (s.originalBasePath) {
594
+ s.basePath = s.originalBasePath;
595
+ try {
596
+ process.chdir(s.basePath);
597
+ } catch {
598
+ /* best-effort */
599
+ }
600
+ }
593
601
  } catch (e) {
594
- debugLog("stop-rebuild-state-failed", {
595
- error: e instanceof Error ? e.message : String(e),
596
- });
602
+ debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
597
603
  }
598
- }
599
604
 
600
- clearCmuxSidebar(loadedPreferences);
601
- logCmuxEvent(
602
- loadedPreferences,
603
- `Auto-mode stopped${reasonSuffix || ""}.`,
604
- reason?.startsWith("Blocked:") ? "warning" : "info",
605
- );
605
+ // ── Step 7: Ledger notification ──
606
+ try {
607
+ const ledger = getLedger();
608
+ if (ledger && ledger.units.length > 0) {
609
+ const totals = getProjectTotals(ledger.units);
610
+ ctx?.ui.notify(
611
+ `Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`,
612
+ "info",
613
+ );
614
+ } else {
615
+ ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
616
+ }
617
+ } catch (e) {
618
+ debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
619
+ }
606
620
 
607
- if (isDebugEnabled()) {
608
- const logPath = writeDebugSummary();
609
- if (logPath) {
610
- ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
621
+ // ── Step 8: Rebuild state ──
622
+ if (s.basePath) {
623
+ try {
624
+ await rebuildState(s.basePath);
625
+ } catch (e) {
626
+ debugLog("stop-rebuild-state-failed", {
627
+ error: e instanceof Error ? e.message : String(e),
628
+ });
629
+ }
611
630
  }
612
- }
613
631
 
614
- resetMetrics();
615
- resetRoutingHistory();
616
- resetHookState();
617
- if (s.basePath) clearPersistedHookState(s.basePath);
632
+ // ── Step 9: Cmux sidebar / event log ──
633
+ try {
634
+ clearCmuxSidebar(loadedPreferences);
635
+ logCmuxEvent(
636
+ loadedPreferences,
637
+ `Auto-mode stopped${reasonSuffix || ""}.`,
638
+ reason?.startsWith("Blocked:") ? "warning" : "info",
639
+ );
640
+ } catch (e) {
641
+ debugLog("stop-cleanup-cmux", { error: e instanceof Error ? e.message : String(e) });
642
+ }
618
643
 
619
- // Remove paused-session metadata if present (#1383)
620
- try {
621
- const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
622
- if (existsSync(pausedPath)) unlinkSync(pausedPath);
623
- } catch { /* non-fatal */ }
644
+ // ── Step 10: Debug summary ──
645
+ try {
646
+ if (isDebugEnabled()) {
647
+ const logPath = writeDebugSummary();
648
+ if (logPath) {
649
+ ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
650
+ }
651
+ }
652
+ } catch (e) {
653
+ debugLog("stop-cleanup-debug", { error: e instanceof Error ? e.message : String(e) });
654
+ }
624
655
 
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);
656
+ // ── Step 11: Reset metrics, routing, hooks ──
657
+ try {
658
+ resetMetrics();
659
+ resetRoutingHistory();
660
+ resetHookState();
661
+ if (s.basePath) clearPersistedHookState(s.basePath);
662
+ } catch (e) {
663
+ debugLog("stop-cleanup-metrics", { error: e instanceof Error ? e.message : String(e) });
664
+ }
650
665
 
651
- if (pi && ctx && s.originalModelId && s.originalModelProvider) {
652
- const original = ctx.modelRegistry.find(
653
- s.originalModelProvider,
654
- s.originalModelId,
655
- );
656
- if (original) await pi.setModel(original);
657
- s.originalModelId = null;
658
- s.originalModelProvider = null;
659
- }
666
+ // ── Step 12: Remove paused-session metadata (#1383) ──
667
+ try {
668
+ const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
669
+ if (existsSync(pausedPath)) unlinkSync(pausedPath);
670
+ } catch { /* non-fatal */ }
660
671
 
661
- s.cmdCtx = null;
672
+ // ── Step 13: Restore original model (before reset clears IDs) ──
673
+ try {
674
+ if (pi && ctx && s.originalModelId && s.originalModelProvider) {
675
+ const original = ctx.modelRegistry.find(
676
+ s.originalModelProvider,
677
+ s.originalModelId,
678
+ );
679
+ if (original) await pi.setModel(original);
680
+ }
681
+ } catch (e) {
682
+ debugLog("stop-cleanup-model", { error: e instanceof Error ? e.message : String(e) });
683
+ }
684
+ } finally {
685
+ // ── Critical invariants: these MUST execute regardless of errors ──
686
+ // External cleanup (not covered by session reset)
687
+ clearInFlightTools();
688
+ clearSliceProgressCache();
689
+ clearActivityLogState();
690
+ resetProactiveHealing();
691
+
692
+ // UI cleanup
693
+ ctx?.ui.setStatus("gsd-auto", undefined);
694
+ ctx?.ui.setWidget("gsd-progress", undefined);
695
+ ctx?.ui.setFooter(undefined);
696
+
697
+ // Reset all session state in one call
698
+ s.reset();
699
+ }
662
700
  }
663
701
 
664
702
  /**
@@ -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);