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.
- package/README.md +1 -1
- package/dist/onboarding.js +1 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +67 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +91 -2
- package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
- package/dist/resources/extensions/gsd/doctor-providers.js +35 -1
- package/dist/resources/extensions/gsd/files.js +41 -0
- package/dist/resources/extensions/gsd/observability-validator.js +24 -0
- package/dist/resources/extensions/gsd/preferences-types.js +2 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +42 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/dist/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
- package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/package.json +2 -1
- package/packages/pi-ai/dist/env-api-keys.js +13 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +172 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +172 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
- package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +47 -764
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
- package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +2 -2
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/package.json +1 -0
- package/packages/pi-ai/src/env-api-keys.ts +14 -0
- package/packages/pi-ai/src/models.generated.ts +172 -0
- package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
- package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
- package/packages/pi-ai/src/providers/anthropic.ts +76 -868
- package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
- package/packages/pi-ai/src/types.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +93 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +125 -3
- package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +38 -1
- package/src/resources/extensions/gsd/files.ts +45 -0
- package/src/resources/extensions/gsd/observability-validator.ts +27 -0
- package/src/resources/extensions/gsd/preferences-types.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +41 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
- package/src/resources/extensions/gsd/prompts/reactive-execute.md +41 -0
- package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +108 -3
- package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
- package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
- 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
|
|
package/dist/onboarding.js
CHANGED
|
@@ -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
|
|
692
|
-
|
|
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}}
|