gsd-pi 2.37.1 → 2.38.0-dev.eeb3520

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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/onboarding.js +1 -0
  3. package/dist/resources/extensions/gsd/auto-dispatch.js +67 -1
  4. package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
  5. package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
  6. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  7. package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
  8. package/dist/resources/extensions/gsd/files.js +41 -0
  9. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  10. package/dist/resources/extensions/gsd/preferences-types.js +2 -1
  11. package/dist/resources/extensions/gsd/preferences-validation.js +42 -0
  12. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  13. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  14. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  15. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  16. package/package.json +2 -1
  17. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  18. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  19. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  20. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  21. package/packages/pi-ai/dist/models.generated.js +172 -0
  22. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  23. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  24. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  25. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  26. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  27. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  28. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  29. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  30. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  31. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  32. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  33. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  34. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  35. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  36. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  37. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  38. package/packages/pi-ai/dist/types.d.ts +2 -2
  39. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  40. package/packages/pi-ai/dist/types.js.map +1 -1
  41. package/packages/pi-ai/package.json +1 -0
  42. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  43. package/packages/pi-ai/src/models.generated.ts +172 -0
  44. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  45. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  46. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  47. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  48. package/packages/pi-ai/src/types.ts +2 -0
  49. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  51. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  52. package/packages/pi-coding-agent/package.json +1 -1
  53. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  54. package/pkg/package.json +1 -1
  55. package/src/resources/extensions/gsd/auto-dispatch.ts +93 -0
  56. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
  57. package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
  58. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  59. package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
  60. package/src/resources/extensions/gsd/files.ts +45 -0
  61. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  62. package/src/resources/extensions/gsd/preferences-types.ts +5 -1
  63. package/src/resources/extensions/gsd/preferences-validation.ts +41 -0
  64. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  65. package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
  66. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  67. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  68. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
  69. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  70. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  71. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  72. package/src/resources/extensions/gsd/types.ts +43 -0
package/README.md CHANGED
@@ -629,7 +629,7 @@ GSD isn't locked to one provider. It runs on the [Pi SDK](https://github.com/bad
629
629
 
630
630
  ### Built-in Providers
631
631
 
632
- Anthropic, OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock, Azure OpenAI, Google Vertex, Groq, Cerebras, Mistral, xAI, HuggingFace, Vercel AI Gateway, and more.
632
+ Anthropic, Anthropic (Vertex AI), OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock, Azure OpenAI, Google Vertex, Groq, Cerebras, Mistral, xAI, HuggingFace, Vercel AI Gateway, and more.
633
633
 
634
634
  ### OAuth / Max Plans
635
635
 
@@ -38,6 +38,7 @@ const TOOL_KEYS = [
38
38
  /** Known LLM provider IDs that, if authed, mean the user doesn't need onboarding */
39
39
  const LLM_PROVIDER_IDS = [
40
40
  'anthropic',
41
+ 'anthropic-vertex',
41
42
  'openai',
42
43
  'github-copilot',
43
44
  'openai-codex',
@@ -12,7 +12,7 @@ import { loadFile, loadActiveOverrides, parseRoadmap } from "./files.js";
12
12
  import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile, relSliceFile, buildMilestoneFileName, } from "./paths.js";
13
13
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
14
14
  import { join } from "node:path";
15
- import { buildResearchMilestonePrompt, buildPlanMilestonePrompt, buildResearchSlicePrompt, buildPlanSlicePrompt, buildExecuteTaskPrompt, buildCompleteSlicePrompt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt, buildReplanSlicePrompt, buildRunUatPrompt, buildReassessRoadmapPrompt, buildRewriteDocsPrompt, checkNeedsReassessment, checkNeedsRunUat, } from "./auto-prompts.js";
15
+ import { buildResearchMilestonePrompt, buildPlanMilestonePrompt, buildResearchSlicePrompt, buildPlanSlicePrompt, buildExecuteTaskPrompt, buildCompleteSlicePrompt, buildCompleteMilestonePrompt, buildValidateMilestonePrompt, buildReplanSlicePrompt, buildRunUatPrompt, buildReassessRoadmapPrompt, buildRewriteDocsPrompt, buildReactiveExecutePrompt, checkNeedsReassessment, checkNeedsRunUat, } from "./auto-prompts.js";
16
16
  function missingSliceStop(mid, phase) {
17
17
  return {
18
18
  action: "stop",
@@ -223,6 +223,72 @@ const DISPATCH_RULES = [
223
223
  };
224
224
  },
225
225
  },
226
+ {
227
+ name: "executing → reactive-execute (parallel dispatch)",
228
+ match: async ({ state, mid, midTitle, basePath, prefs }) => {
229
+ if (state.phase !== "executing" || !state.activeTask)
230
+ return null;
231
+ if (!state.activeSlice)
232
+ return null; // fall through
233
+ // Only activate when reactive_execution is explicitly enabled
234
+ const reactiveConfig = prefs?.reactive_execution;
235
+ if (!reactiveConfig?.enabled)
236
+ return null;
237
+ const sid = state.activeSlice.id;
238
+ const sTitle = state.activeSlice.title;
239
+ const maxParallel = reactiveConfig.max_parallel ?? 2;
240
+ // Dry-run mode: max_parallel=1 means graph is derived and logged but
241
+ // execution remains sequential
242
+ if (maxParallel <= 1)
243
+ return null;
244
+ try {
245
+ const { loadSliceTaskIO, deriveTaskGraph, isGraphAmbiguous, getReadyTasks, chooseNonConflictingSubset, graphMetrics, } = await import("./reactive-graph.js");
246
+ const taskIO = await loadSliceTaskIO(basePath, mid, sid);
247
+ if (taskIO.length < 2)
248
+ return null; // single task, no point
249
+ const graph = deriveTaskGraph(taskIO);
250
+ // Ambiguous graph → fall through to sequential
251
+ if (isGraphAmbiguous(graph))
252
+ return null;
253
+ const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
254
+ const readyIds = getReadyTasks(graph, completed, new Set());
255
+ // Only activate reactive dispatch when >1 task is ready
256
+ if (readyIds.length <= 1)
257
+ return null;
258
+ const selected = chooseNonConflictingSubset(readyIds, graph, maxParallel, new Set());
259
+ if (selected.length <= 1)
260
+ return null;
261
+ // Log graph metrics for observability
262
+ const metrics = graphMetrics(graph);
263
+ process.stderr.write(`gsd-reactive: ${mid}/${sid} graph — tasks:${metrics.taskCount} edges:${metrics.edgeCount} ` +
264
+ `ready:${metrics.readySetSize} dispatching:${selected.length} ambiguous:${metrics.ambiguous}\n`);
265
+ // Persist dispatched batch so verification and recovery can check
266
+ // exactly which tasks were sent.
267
+ const { saveReactiveState } = await import("./reactive-graph.js");
268
+ saveReactiveState(basePath, mid, sid, {
269
+ sliceId: sid,
270
+ completed: [...completed],
271
+ dispatched: selected,
272
+ graphSnapshot: metrics,
273
+ updatedAt: new Date().toISOString(),
274
+ });
275
+ // Encode selected task IDs in unitId for artifact verification.
276
+ // Format: M001/S01/reactive+T02,T03
277
+ const batchSuffix = selected.join(",");
278
+ return {
279
+ action: "dispatch",
280
+ unitType: "reactive-execute",
281
+ unitId: `${mid}/${sid}/reactive+${batchSuffix}`,
282
+ prompt: await buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, selected, basePath),
283
+ };
284
+ }
285
+ catch (err) {
286
+ // Non-fatal — fall through to sequential execution
287
+ process.stderr.write(`gsd-reactive: graph derivation failed: ${err.message}\n`);
288
+ return null;
289
+ }
290
+ },
291
+ },
226
292
  {
227
293
  name: "executing → execute-task (recover missing task plan → plan-slice)",
228
294
  match: async ({ state, mid, midTitle, basePath }) => {
@@ -171,6 +171,20 @@ export async function postUnitPreVerification(pctx) {
171
171
  // Non-fatal
172
172
  }
173
173
  }
174
+ // Reactive state cleanup on slice completion
175
+ if (s.currentUnit.type === "complete-slice") {
176
+ try {
177
+ const parts = s.currentUnit.id.split("/");
178
+ const [mid, sid] = parts;
179
+ if (mid && sid) {
180
+ const { clearReactiveState } = await import("./reactive-graph.js");
181
+ clearReactiveState(s.basePath, mid, sid);
182
+ }
183
+ }
184
+ catch {
185
+ // Non-fatal
186
+ }
187
+ }
174
188
  // Post-triage: execute actionable resolutions
175
189
  if (s.currentUnit.type === "triage-captures") {
176
190
  try {
@@ -414,6 +414,35 @@ export async function getPriorTaskSummaryPaths(mid, sid, currentTid, base) {
414
414
  })
415
415
  .map(f => `${sRel}/tasks/${f}`);
416
416
  }
417
+ /**
418
+ * Get carry-forward summary paths scoped to a task's derived dependencies.
419
+ *
420
+ * Instead of all prior tasks (order-based), returns only summaries for task
421
+ * IDs in `dependsOn`. Used by reactive-execute to give each subagent only
422
+ * the context it actually needs — not sibling tasks from a parallel batch.
423
+ *
424
+ * Falls back to order-based when dependsOn is empty (root tasks still get
425
+ * any available prior summaries for continuity).
426
+ */
427
+ export async function getDependencyTaskSummaryPaths(mid, sid, currentTid, dependsOn, base) {
428
+ // If no dependencies, fall back to order-based for root tasks
429
+ if (dependsOn.length === 0) {
430
+ return getPriorTaskSummaryPaths(mid, sid, currentTid, base);
431
+ }
432
+ const tDir = resolveTasksDir(base, mid, sid);
433
+ if (!tDir)
434
+ return [];
435
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
436
+ const sRel = relSlicePath(base, mid, sid);
437
+ const depSet = new Set(dependsOn.map((d) => d.toUpperCase()));
438
+ return summaryFiles
439
+ .filter((f) => {
440
+ // Extract task ID from filename: "T02-SUMMARY.md" → "T02"
441
+ const tid = f.replace(/-SUMMARY\.md$/i, "").toUpperCase();
442
+ return depSet.has(tid);
443
+ })
444
+ .map((f) => `${sRel}/tasks/${f}`);
445
+ }
417
446
  // ─── Adaptive Replanning Checks ────────────────────────────────────────────
418
447
  /**
419
448
  * Check if the most recently completed slice needs reassessment.
@@ -688,8 +717,11 @@ export async function buildPlanSlicePrompt(mid, _midTitle, sid, sTitle, base, le
688
717
  });
689
718
  }
690
719
  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);
720
+ const opts = typeof level === "object" && level !== null && !Array.isArray(level)
721
+ ? level
722
+ : { level: level };
723
+ const inlineLevel = opts.level ?? resolveInlineLevel();
724
+ const priorSummaries = opts.carryForwardPaths ?? await getPriorTaskSummaryPaths(mid, sid, tid, base);
693
725
  const priorLines = priorSummaries.length > 0
694
726
  ? priorSummaries.map(p => `- \`${p}\``).join("\n")
695
727
  : "- (no prior tasks)";
@@ -1090,6 +1122,63 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
1090
1122
  commitInstruction: reassessCommitInstruction,
1091
1123
  });
1092
1124
  }
1125
+ // ─── Reactive Execute Prompt ──────────────────────────────────────────────
1126
+ export async function buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, readyTaskIds, base) {
1127
+ const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
1128
+ // Build graph for context
1129
+ const taskIO = await loadSliceTaskIO(base, mid, sid);
1130
+ const graph = deriveTaskGraph(taskIO);
1131
+ const metrics = graphMetrics(graph);
1132
+ // Build graph context section
1133
+ const graphLines = [];
1134
+ for (const node of graph) {
1135
+ const status = node.done ? "✅ done" : readyTaskIds.includes(node.id) ? "🟢 ready" : "⏳ waiting";
1136
+ const deps = node.dependsOn.length > 0 ? ` (depends on: ${node.dependsOn.join(", ")})` : "";
1137
+ graphLines.push(`- **${node.id}: ${node.title}** — ${status}${deps}`);
1138
+ if (node.outputFiles.length > 0) {
1139
+ graphLines.push(` - Outputs: ${node.outputFiles.map(f => `\`${f}\``).join(", ")}`);
1140
+ }
1141
+ }
1142
+ const graphContext = [
1143
+ `Tasks: ${metrics.taskCount}, Edges: ${metrics.edgeCount}, Ready: ${metrics.readySetSize}`,
1144
+ "",
1145
+ ...graphLines,
1146
+ ].join("\n");
1147
+ // Build individual subagent prompts for each ready task
1148
+ const subagentSections = [];
1149
+ const readyTaskListLines = [];
1150
+ for (const tid of readyTaskIds) {
1151
+ const node = graph.find((n) => n.id === tid);
1152
+ const tTitle = node?.title ?? tid;
1153
+ readyTaskListLines.push(`- **${tid}: ${tTitle}**`);
1154
+ // Build dependency-scoped carry-forward paths for this task
1155
+ const depPaths = await getDependencyTaskSummaryPaths(mid, sid, tid, node?.dependsOn ?? [], base);
1156
+ // Build a full execute-task prompt with dependency-based carry-forward
1157
+ const taskPrompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base, { carryForwardPaths: depPaths });
1158
+ subagentSections.push([
1159
+ `### ${tid}: ${tTitle}`,
1160
+ "",
1161
+ "Use this as the prompt for a `subagent` call:",
1162
+ "",
1163
+ "```",
1164
+ taskPrompt,
1165
+ "```",
1166
+ ].join("\n"));
1167
+ }
1168
+ const inlinedTemplates = inlineTemplate("task-summary", "Task Summary");
1169
+ return loadPrompt("reactive-execute", {
1170
+ workingDirectory: base,
1171
+ milestoneId: mid,
1172
+ milestoneTitle: midTitle,
1173
+ sliceId: sid,
1174
+ sliceTitle: sTitle,
1175
+ graphContext,
1176
+ readyTaskCount: String(readyTaskIds.length),
1177
+ readyTaskList: readyTaskListLines.join("\n"),
1178
+ subagentPrompts: subagentSections.join("\n\n---\n\n"),
1179
+ inlinedTemplates,
1180
+ });
1181
+ }
1093
1182
  export async function buildRewriteDocsPrompt(mid, midTitle, activeSlice, base, overrides) {
1094
1183
  const sid = activeSlice?.id;
1095
1184
  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).
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { existsSync } from "node:fs";
14
14
  import { AuthStorage } from "@gsd/pi-coding-agent";
15
+ import { getEnvApiKey } from "@gsd/pi-ai";
15
16
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
17
  import { getAuthPath, PROVIDER_REGISTRY } from "./key-manager.js";
17
18
  // ── Model → Provider ID mapping ───────────────────────────────────────────────
@@ -33,6 +34,7 @@ function modelToProviderId(model) {
33
34
  google: "google",
34
35
  anthropic: "anthropic",
35
36
  openai: "openai",
37
+ "github-copilot": "github-copilot",
36
38
  };
37
39
  if (prefixMap[prefix])
38
40
  return prefixMap[prefix];
@@ -108,13 +110,29 @@ function resolveKey(providerId) {
108
110
  // auth.json malformed — fall through to env check
109
111
  }
110
112
  }
111
- // Check environment variable
113
+ // Check environment variable using the authoritative env var resolution
114
+ // (handles multi-var lookups like ANTHROPIC_OAUTH_TOKEN || ANTHROPIC_API_KEY,
115
+ // COPILOT_GITHUB_TOKEN || GH_TOKEN || GITHUB_TOKEN, Vertex ADC, Bedrock, etc.)
116
+ if (getEnvApiKey(providerId)) {
117
+ return { found: true, source: "env", backedOff: false };
118
+ }
119
+ // Fall back to PROVIDER_REGISTRY env var for providers not covered by getEnvApiKey
120
+ // (e.g., search providers like Brave, Tavily; tool providers like Jina, Context7)
112
121
  if (info?.envVar && process.env[info.envVar]) {
113
122
  return { found: true, source: "env", backedOff: false };
114
123
  }
115
124
  return { found: false, source: "none", backedOff: false };
116
125
  }
117
126
  // ── Individual check groups ────────────────────────────────────────────────────
127
+ /**
128
+ * Providers that can serve models normally associated with another provider.
129
+ * Key = the provider whose models can be served, Value = alternative providers to check.
130
+ * e.g. GitHub Copilot subscriptions can access Claude and GPT models.
131
+ */
132
+ const PROVIDER_ROUTES = {
133
+ anthropic: ["github-copilot"],
134
+ openai: ["github-copilot"],
135
+ };
118
136
  function checkLlmProviders() {
119
137
  const required = collectConfiguredModelProviders();
120
138
  const results = [];
@@ -123,6 +141,22 @@ function checkLlmProviders() {
123
141
  const label = info?.label ?? providerId;
124
142
  const lookup = resolveKey(providerId);
125
143
  if (!lookup.found) {
144
+ // Check if a cross-provider can serve this provider's models
145
+ const routes = PROVIDER_ROUTES[providerId];
146
+ const routeProvider = routes?.find(routeId => resolveKey(routeId).found);
147
+ if (routeProvider) {
148
+ const routeInfo = PROVIDER_REGISTRY.find(p => p.id === routeProvider);
149
+ const routeLabel = routeInfo?.label ?? routeProvider;
150
+ results.push({
151
+ name: providerId,
152
+ label,
153
+ category: "llm",
154
+ status: "ok",
155
+ message: `${label} — available via ${routeLabel}`,
156
+ required: true,
157
+ });
158
+ continue;
159
+ }
126
160
  const envVar = info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
127
161
  results.push({
128
162
  name: providerId,
@@ -629,6 +629,47 @@ export function countMustHavesMentionedInSummary(mustHaves, summaryContent) {
629
629
  }
630
630
  return count;
631
631
  }
632
+ // ─── Task Plan IO Extractor ────────────────────────────────────────────────
633
+ /**
634
+ * Extract input and output file paths from a task plan's `## Inputs` and
635
+ * `## Expected Output` sections. Looks for backtick-wrapped file paths on
636
+ * each line (e.g. `` `src/foo.ts` ``).
637
+ *
638
+ * Returns empty arrays for missing/empty sections — callers should treat
639
+ * tasks with no IO as ambiguous (sequential fallback trigger).
640
+ */
641
+ export function parseTaskPlanIO(content) {
642
+ const backtickPathRegex = /`([^`]+)`/g;
643
+ function extractPaths(sectionText) {
644
+ if (!sectionText)
645
+ return [];
646
+ const paths = [];
647
+ for (const line of sectionText.split("\n")) {
648
+ const trimmed = line.trim();
649
+ if (!trimmed || trimmed.startsWith("#"))
650
+ continue;
651
+ let match;
652
+ backtickPathRegex.lastIndex = 0;
653
+ while ((match = backtickPathRegex.exec(trimmed)) !== null) {
654
+ const candidate = match[1];
655
+ // Filter out things that look like code tokens rather than file paths
656
+ // (e.g. `true`, `false`, `npm run test`). A file path has at least one
657
+ // dot or slash.
658
+ if (candidate.includes("/") || candidate.includes(".")) {
659
+ paths.push(candidate);
660
+ }
661
+ }
662
+ }
663
+ return paths;
664
+ }
665
+ const [, body] = splitFrontmatter(content);
666
+ const inputSection = extractSection(body, "Inputs");
667
+ const outputSection = extractSection(body, "Expected Output");
668
+ return {
669
+ inputFiles: extractPaths(inputSection),
670
+ outputFiles: extractPaths(outputSection),
671
+ };
672
+ }
632
673
  /**
633
674
  * Extract the UAT type from a UAT file's raw content.
634
675
  *
@@ -209,6 +209,30 @@ export function validateTaskPlanContent(file, content) {
209
209
  }
210
210
  }
211
211
  }
212
+ // Rule: Inputs and Expected Output should contain backtick-wrapped file paths
213
+ const inputsSection = getSection(content, "Inputs", 2);
214
+ const outputSection = getSection(content, "Expected Output", 2);
215
+ const backtickPathPattern = /`[^`]*[./][^`]*`/;
216
+ if (outputSection === null || !backtickPathPattern.test(outputSection)) {
217
+ issues.push({
218
+ severity: "warning",
219
+ scope: "task-plan",
220
+ file,
221
+ ruleId: "missing_output_file_paths",
222
+ message: "Task plan `## Expected Output` is missing or has no backtick-wrapped file paths.",
223
+ suggestion: "List concrete output file paths in backticks (e.g. `src/types.ts`). These are machine-parsed to derive task dependencies.",
224
+ });
225
+ }
226
+ if (inputsSection !== null && inputsSection.trim().length > 0 && !backtickPathPattern.test(inputsSection)) {
227
+ issues.push({
228
+ severity: "info",
229
+ scope: "task-plan",
230
+ file,
231
+ ruleId: "missing_input_file_paths",
232
+ message: "Task plan `## Inputs` has content but no backtick-wrapped file paths.",
233
+ suggestion: "List input file paths in backticks (e.g. `src/config.json`). These are machine-parsed to derive task dependencies.",
234
+ });
235
+ }
212
236
  // ── Observability rules (gated by runtime relevance) ──
213
237
  const relevant = textSuggestsObservabilityRelevant(content);
214
238
  if (!relevant)
@@ -65,11 +65,12 @@ export const KNOWN_PREFERENCE_KEYS = new Set([
65
65
  "compression_strategy",
66
66
  "context_selection",
67
67
  "widget_mode",
68
+ "reactive_execution",
68
69
  ]);
69
70
  /** Canonical list of all dispatch unit types. */
70
71
  export const KNOWN_UNIT_TYPES = [
71
72
  "research-milestone", "plan-milestone", "research-slice", "plan-slice",
72
- "execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
73
+ "execute-task", "reactive-execute", "complete-slice", "replan-slice", "reassess-roadmap",
73
74
  "run-uat", "complete-milestone",
74
75
  ];
75
76
  export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
@@ -500,6 +500,48 @@ export function validatePreferences(preferences) {
500
500
  validated.parallel = parallel;
501
501
  }
502
502
  }
503
+ // ─── Reactive Execution ─────────────────────────────────────────────────
504
+ if (preferences.reactive_execution !== undefined) {
505
+ if (typeof preferences.reactive_execution === "object" && preferences.reactive_execution !== null) {
506
+ const re = preferences.reactive_execution;
507
+ const validRe = {};
508
+ if (re.enabled !== undefined) {
509
+ if (typeof re.enabled === "boolean")
510
+ validRe.enabled = re.enabled;
511
+ else
512
+ errors.push("reactive_execution.enabled must be a boolean");
513
+ }
514
+ if (re.max_parallel !== undefined) {
515
+ const mp = typeof re.max_parallel === "number" ? re.max_parallel : Number(re.max_parallel);
516
+ if (Number.isFinite(mp) && mp >= 1 && mp <= 8) {
517
+ validRe.max_parallel = Math.floor(mp);
518
+ }
519
+ else {
520
+ errors.push("reactive_execution.max_parallel must be a number between 1 and 8");
521
+ }
522
+ }
523
+ if (re.isolation_mode !== undefined) {
524
+ if (re.isolation_mode === "same-tree") {
525
+ validRe.isolation_mode = "same-tree";
526
+ }
527
+ else {
528
+ errors.push('reactive_execution.isolation_mode must be "same-tree"');
529
+ }
530
+ }
531
+ const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode"]);
532
+ for (const key of Object.keys(re)) {
533
+ if (!knownReKeys.has(key)) {
534
+ warnings.push(`unknown reactive_execution key "${key}" — ignored`);
535
+ }
536
+ }
537
+ if (Object.keys(validRe).length > 0) {
538
+ validated.reactive_execution = validRe;
539
+ }
540
+ }
541
+ else {
542
+ errors.push("reactive_execution must be an object");
543
+ }
544
+ }
503
545
  // ─── Verification Preferences ───────────────────────────────────────────
504
546
  if (preferences.verification_commands !== undefined) {
505
547
  if (Array.isArray(preferences.verification_commands)) {
@@ -61,13 +61,14 @@ Then:
61
61
  - a concrete, action-oriented title
62
62
  - the inline task entry fields defined in the plan.md template (Why / Files / Do / Verify / Done when)
63
63
  - a matching task plan file with description, steps, must-haves, verification, inputs, and expected output
64
+ - **Inputs and Expected Output must list concrete backtick-wrapped file paths** (e.g. `` `src/types.ts` ``). These are machine-parsed to derive task dependencies — vague prose without paths breaks parallel execution. Every task must have at least one output file path.
64
65
  - Observability Impact section **only if the task touches runtime boundaries, async flows, or error paths** — omit it otherwise
65
66
  6. Write `{{outputPath}}`
66
67
  7. Write individual task plans in `{{slicePath}}/tasks/`: `T01-PLAN.md`, `T02-PLAN.md`, etc.
67
68
  8. **Self-audit the plan.** Walk through each check — if any fail, fix the plan files before moving on:
68
69
  - **Completion semantics:** If every task were completed exactly as written, the slice goal/demo should actually be true.
69
70
  - **Requirement coverage:** Every must-have in the slice maps to at least one task. No must-have is orphaned. If `REQUIREMENTS.md` exists, every Active requirement this slice owns maps to at least one task.
70
- - **Task completeness:** Every task has steps, must-haves, verification, inputs, and expected output — none are blank or vague.
71
+ - **Task completeness:** Every task has steps, must-haves, verification, inputs, and expected output — none are blank or vague. Inputs and Expected Output list backtick-wrapped file paths, not prose descriptions.
71
72
  - **Dependency correctness:** Task ordering is consistent. No task references work from a later task.
72
73
  - **Key links planned:** For every pair of artifacts that must connect, there is an explicit step that wires them.
73
74
  - **Scope sanity:** Target 2–5 steps and 3–8 files per task. 10+ steps or 12+ files — must split. Each task must be completable in a single fresh context window.
@@ -0,0 +1,41 @@
1
+ # Reactive Task Execution — Parallel Dispatch
2
+
3
+ **Working directory:** `{{workingDirectory}}`
4
+ **Milestone:** {{milestoneId}} — {{milestoneTitle}}
5
+ **Slice:** {{sliceId}} — {{sliceTitle}}
6
+
7
+ ## Mission
8
+
9
+ You are executing **multiple tasks in parallel** for this slice. The task graph below shows which tasks are ready for simultaneous execution based on their input/output dependencies.
10
+
11
+ **Critical rule:** Use the `subagent` tool in **parallel mode** to dispatch all ready tasks simultaneously. Each subagent gets a self-contained execute-task prompt. After all subagents return, verify each task's outputs and write summaries.
12
+
13
+ ## Task Dependency Graph
14
+
15
+ {{graphContext}}
16
+
17
+ ## Ready Tasks for Parallel Dispatch
18
+
19
+ {{readyTaskCount}} tasks are ready for parallel execution:
20
+
21
+ {{readyTaskList}}
22
+
23
+ ## Execution Protocol
24
+
25
+ 1. **Dispatch all ready tasks** using `subagent` in parallel mode. Each subagent prompt is provided below.
26
+ 2. **Wait for all subagents** to complete.
27
+ 3. **Verify each task's outputs** — check that expected files were created/modified and that verification commands pass.
28
+ 4. **Write task summaries** for each completed task using the task-summary template.
29
+ 5. **Mark completed tasks** as done in the slice plan (checkbox `[x]`).
30
+ 6. **Commit** all changes with a clear message covering the parallel batch.
31
+
32
+ If any subagent fails:
33
+ - Write a summary for the failed task with `blocker_discovered: true`
34
+ - Continue marking the successful tasks as done
35
+ - The orchestrator will handle re-dispatch on the next iteration
36
+
37
+ ## Subagent Prompts
38
+
39
+ {{subagentPrompts}}
40
+
41
+ {{inlinedTemplates}}