gsd-pi 2.37.1-dev.193bd3d → 2.37.1-dev.49503be
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 +54 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +14 -0
- package/dist/resources/extensions/gsd/auto-prompts.js +55 -0
- package/dist/resources/extensions/gsd/auto-recovery.js +19 -1
- package/dist/resources/extensions/gsd/files.js +41 -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/reactive-execute.md +41 -0
- package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
- 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/src/core/model-resolver.ts +1 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +78 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +14 -0
- package/src/resources/extensions/gsd/auto-prompts.ts +68 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +18 -0
- package/src/resources/extensions/gsd/files.ts +45 -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/reactive-execute.md +41 -0
- package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
- package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +367 -0
- package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
- package/src/resources/extensions/gsd/types.ts +41 -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,59 @@ 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
|
+
return {
|
|
266
|
+
action: "dispatch",
|
|
267
|
+
unitType: "reactive-execute",
|
|
268
|
+
unitId: `${mid}/${sid}/reactive`,
|
|
269
|
+
prompt: await buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, selected, basePath),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
// Non-fatal — fall through to sequential execution
|
|
274
|
+
process.stderr.write(`gsd-reactive: graph derivation failed: ${err.message}\n`);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
},
|
|
226
279
|
{
|
|
227
280
|
name: "executing → execute-task (recover missing task plan → plan-slice)",
|
|
228
281
|
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 {
|
|
@@ -1090,6 +1090,61 @@ export async function buildReassessRoadmapPrompt(mid, midTitle, completedSliceId
|
|
|
1090
1090
|
commitInstruction: reassessCommitInstruction,
|
|
1091
1091
|
});
|
|
1092
1092
|
}
|
|
1093
|
+
// ─── Reactive Execute Prompt ──────────────────────────────────────────────
|
|
1094
|
+
export async function buildReactiveExecutePrompt(mid, midTitle, sid, sTitle, readyTaskIds, base) {
|
|
1095
|
+
const { loadSliceTaskIO, deriveTaskGraph, graphMetrics } = await import("./reactive-graph.js");
|
|
1096
|
+
// Build graph for context
|
|
1097
|
+
const taskIO = await loadSliceTaskIO(base, mid, sid);
|
|
1098
|
+
const graph = deriveTaskGraph(taskIO);
|
|
1099
|
+
const metrics = graphMetrics(graph);
|
|
1100
|
+
// Build graph context section
|
|
1101
|
+
const graphLines = [];
|
|
1102
|
+
for (const node of graph) {
|
|
1103
|
+
const status = node.done ? "✅ done" : readyTaskIds.includes(node.id) ? "🟢 ready" : "⏳ waiting";
|
|
1104
|
+
const deps = node.dependsOn.length > 0 ? ` (depends on: ${node.dependsOn.join(", ")})` : "";
|
|
1105
|
+
graphLines.push(`- **${node.id}: ${node.title}** — ${status}${deps}`);
|
|
1106
|
+
if (node.outputFiles.length > 0) {
|
|
1107
|
+
graphLines.push(` - Outputs: ${node.outputFiles.map(f => `\`${f}\``).join(", ")}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const graphContext = [
|
|
1111
|
+
`Tasks: ${metrics.taskCount}, Edges: ${metrics.edgeCount}, Ready: ${metrics.readySetSize}`,
|
|
1112
|
+
"",
|
|
1113
|
+
...graphLines,
|
|
1114
|
+
].join("\n");
|
|
1115
|
+
// Build individual subagent prompts for each ready task
|
|
1116
|
+
const subagentSections = [];
|
|
1117
|
+
const readyTaskListLines = [];
|
|
1118
|
+
for (const tid of readyTaskIds) {
|
|
1119
|
+
const node = graph.find((n) => n.id === tid);
|
|
1120
|
+
const tTitle = node?.title ?? tid;
|
|
1121
|
+
readyTaskListLines.push(`- **${tid}: ${tTitle}**`);
|
|
1122
|
+
// Build a full execute-task prompt for this task (reuse existing builder)
|
|
1123
|
+
const taskPrompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base);
|
|
1124
|
+
subagentSections.push([
|
|
1125
|
+
`### ${tid}: ${tTitle}`,
|
|
1126
|
+
"",
|
|
1127
|
+
"Use this as the prompt for a `subagent` call:",
|
|
1128
|
+
"",
|
|
1129
|
+
"```",
|
|
1130
|
+
taskPrompt,
|
|
1131
|
+
"```",
|
|
1132
|
+
].join("\n"));
|
|
1133
|
+
}
|
|
1134
|
+
const inlinedTemplates = inlineTemplate("task-summary", "Task Summary");
|
|
1135
|
+
return loadPrompt("reactive-execute", {
|
|
1136
|
+
workingDirectory: base,
|
|
1137
|
+
milestoneId: mid,
|
|
1138
|
+
milestoneTitle: midTitle,
|
|
1139
|
+
sliceId: sid,
|
|
1140
|
+
sliceTitle: sTitle,
|
|
1141
|
+
graphContext,
|
|
1142
|
+
readyTaskCount: String(readyTaskIds.length),
|
|
1143
|
+
readyTaskList: readyTaskListLines.join("\n"),
|
|
1144
|
+
subagentPrompts: subagentSections.join("\n\n---\n\n"),
|
|
1145
|
+
inlinedTemplates,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1093
1148
|
export async function buildRewriteDocsPrompt(mid, midTitle, activeSlice, base, overrides) {
|
|
1094
1149
|
const sid = activeSlice?.id;
|
|
1095
1150
|
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,21 @@ 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 at least one new task summary was written.
|
|
112
|
+
// The unitId is "{mid}/{sid}/reactive" — extract mid and sid to check.
|
|
113
|
+
if (unitType === "reactive-execute") {
|
|
114
|
+
const parts = unitId.split("/");
|
|
115
|
+
const mid = parts[0];
|
|
116
|
+
const sid = parts[1];
|
|
117
|
+
if (!mid || !sid)
|
|
118
|
+
return false;
|
|
119
|
+
const tDir = resolveTasksDir(base, mid, sid);
|
|
120
|
+
if (!tDir)
|
|
121
|
+
return false;
|
|
122
|
+
const summaryFiles = resolveTaskFiles(tDir, "SUMMARY");
|
|
123
|
+
// At least one summary file should exist
|
|
124
|
+
return summaryFiles.length > 0;
|
|
125
|
+
}
|
|
108
126
|
const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
109
127
|
// For unit types with no verifiable artifact (null path), the parent directory
|
|
110
128
|
// is missing on disk — treat as stale completion state so the key gets evicted (#313).
|
|
@@ -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
|
*
|
|
@@ -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)) {
|
|
@@ -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}}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive Task Graph — derives dependency edges from task plan IO signatures.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions that build a DAG from task IO intersections and resolve
|
|
5
|
+
* which tasks are currently ready for parallel dispatch. Used by the
|
|
6
|
+
* reactive-execute dispatch path (ADR-004).
|
|
7
|
+
*
|
|
8
|
+
* Graph derivation and resolution functions are pure (no filesystem access).
|
|
9
|
+
* The `loadSliceTaskIO` loader at the bottom is the only async/IO function.
|
|
10
|
+
*/
|
|
11
|
+
import { loadFile, parsePlan, parseTaskPlanIO } from "./files.js";
|
|
12
|
+
import { resolveTasksDir, resolveTaskFiles } from "./paths.js";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
|
|
15
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
16
|
+
// ─── Graph Construction ───────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Build a dependency graph from task IO signatures.
|
|
19
|
+
*
|
|
20
|
+
* A task T_b depends on T_a when any of T_b's inputFiles appear in T_a's
|
|
21
|
+
* outputFiles. Self-references are excluded.
|
|
22
|
+
*
|
|
23
|
+
* Tasks are returned in the same order as the input array.
|
|
24
|
+
*/
|
|
25
|
+
export function deriveTaskGraph(tasks) {
|
|
26
|
+
// Build output → producer lookup
|
|
27
|
+
const outputToProducer = new Map();
|
|
28
|
+
for (const task of tasks) {
|
|
29
|
+
for (const outFile of task.outputFiles) {
|
|
30
|
+
const existing = outputToProducer.get(outFile);
|
|
31
|
+
if (existing) {
|
|
32
|
+
existing.push(task.id);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
outputToProducer.set(outFile, [task.id]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return tasks.map((task) => {
|
|
40
|
+
const deps = new Set();
|
|
41
|
+
for (const inFile of task.inputFiles) {
|
|
42
|
+
const producers = outputToProducer.get(inFile);
|
|
43
|
+
if (producers) {
|
|
44
|
+
for (const pid of producers) {
|
|
45
|
+
if (pid !== task.id)
|
|
46
|
+
deps.add(pid);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
...task,
|
|
52
|
+
dependsOn: [...deps].sort(),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// ─── Ready Set Resolution ─────────────────────────────────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Return task IDs whose dependencies are all in `completed`.
|
|
59
|
+
* Excludes tasks that are already done or in-flight.
|
|
60
|
+
*/
|
|
61
|
+
export function getReadyTasks(graph, completed, inFlight) {
|
|
62
|
+
return graph
|
|
63
|
+
.filter((node) => {
|
|
64
|
+
if (node.done || completed.has(node.id) || inFlight.has(node.id))
|
|
65
|
+
return false;
|
|
66
|
+
return node.dependsOn.every((dep) => completed.has(dep));
|
|
67
|
+
})
|
|
68
|
+
.map((node) => node.id);
|
|
69
|
+
}
|
|
70
|
+
// ─── Conflict-Free Subset Selection ──────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* Greedy selection of non-conflicting tasks up to `maxParallel`.
|
|
73
|
+
*
|
|
74
|
+
* Two tasks conflict if they share any outputFile. We also exclude tasks
|
|
75
|
+
* whose outputs overlap with `inFlightOutputs` (files being written by
|
|
76
|
+
* tasks currently in progress).
|
|
77
|
+
*/
|
|
78
|
+
export function chooseNonConflictingSubset(readyIds, graph, maxParallel, inFlightOutputs) {
|
|
79
|
+
const nodeMap = new Map(graph.map((n) => [n.id, n]));
|
|
80
|
+
const claimed = new Set(inFlightOutputs);
|
|
81
|
+
const selected = [];
|
|
82
|
+
for (const id of readyIds) {
|
|
83
|
+
if (selected.length >= maxParallel)
|
|
84
|
+
break;
|
|
85
|
+
const node = nodeMap.get(id);
|
|
86
|
+
if (!node)
|
|
87
|
+
continue;
|
|
88
|
+
// Check for output overlap with already-selected or in-flight
|
|
89
|
+
const conflicts = node.outputFiles.some((f) => claimed.has(f));
|
|
90
|
+
if (conflicts)
|
|
91
|
+
continue;
|
|
92
|
+
// Claim this task's outputs
|
|
93
|
+
for (const f of node.outputFiles)
|
|
94
|
+
claimed.add(f);
|
|
95
|
+
selected.push(id);
|
|
96
|
+
}
|
|
97
|
+
return selected;
|
|
98
|
+
}
|
|
99
|
+
// ─── Graph Quality Checks ─────────────────────────────────────────────────
|
|
100
|
+
/**
|
|
101
|
+
* Returns true if any incomplete task has 0 inputFiles AND 0 outputFiles.
|
|
102
|
+
*
|
|
103
|
+
* An ambiguous graph means IO annotations are too sparse to derive reliable
|
|
104
|
+
* edges — the dispatcher should fall back to sequential execution.
|
|
105
|
+
*/
|
|
106
|
+
export function isGraphAmbiguous(graph) {
|
|
107
|
+
return graph.some((node) => !node.done &&
|
|
108
|
+
node.inputFiles.length === 0 &&
|
|
109
|
+
node.outputFiles.length === 0);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Detect deadlock: no tasks are ready and none are in-flight, yet incomplete
|
|
113
|
+
* tasks remain. This indicates a circular dependency or impossible state.
|
|
114
|
+
*/
|
|
115
|
+
export function detectDeadlock(graph, completed, inFlight) {
|
|
116
|
+
const incomplete = graph.filter((n) => !n.done && !completed.has(n.id) && !inFlight.has(n.id));
|
|
117
|
+
if (incomplete.length === 0)
|
|
118
|
+
return false; // all done
|
|
119
|
+
if (inFlight.size > 0)
|
|
120
|
+
return false; // something is running, wait for it
|
|
121
|
+
// Nothing in flight, but incomplete tasks remain — check if any are ready
|
|
122
|
+
const ready = getReadyTasks(graph, completed, inFlight);
|
|
123
|
+
return ready.length === 0;
|
|
124
|
+
}
|
|
125
|
+
// ─── Graph Metrics ────────────────────────────────────────────────────────
|
|
126
|
+
/** Compute summary metrics for logging. */
|
|
127
|
+
export function graphMetrics(graph) {
|
|
128
|
+
const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
|
|
129
|
+
const ready = getReadyTasks(graph, completed, new Set());
|
|
130
|
+
const edgeCount = graph.reduce((sum, n) => sum + n.dependsOn.length, 0);
|
|
131
|
+
return {
|
|
132
|
+
taskCount: graph.length,
|
|
133
|
+
edgeCount,
|
|
134
|
+
readySetSize: ready.length,
|
|
135
|
+
ambiguous: isGraphAmbiguous(graph),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// ─── IO Loader (async, filesystem) ────────────────────────────────────────
|
|
139
|
+
/**
|
|
140
|
+
* Load TaskIO for all tasks in a slice by reading the slice plan (for done
|
|
141
|
+
* status and task IDs) and individual task plan files (for IO sections).
|
|
142
|
+
*
|
|
143
|
+
* Returns [] when the slice plan or tasks directory doesn't exist.
|
|
144
|
+
*/
|
|
145
|
+
export async function loadSliceTaskIO(basePath, mid, sid) {
|
|
146
|
+
const { resolveSliceFile } = await import("./paths.js");
|
|
147
|
+
const slicePlanPath = resolveSliceFile(basePath, mid, sid, "PLAN");
|
|
148
|
+
const planContent = slicePlanPath ? await loadFile(slicePlanPath) : null;
|
|
149
|
+
if (!planContent)
|
|
150
|
+
return [];
|
|
151
|
+
const plan = parsePlan(planContent);
|
|
152
|
+
const tDir = resolveTasksDir(basePath, mid, sid);
|
|
153
|
+
if (!tDir)
|
|
154
|
+
return [];
|
|
155
|
+
const results = [];
|
|
156
|
+
for (const taskEntry of plan.tasks) {
|
|
157
|
+
const planFiles = resolveTaskFiles(tDir, "PLAN");
|
|
158
|
+
const taskFileName = planFiles.find((f) => f.toUpperCase().startsWith(taskEntry.id.toUpperCase() + "-"));
|
|
159
|
+
if (!taskFileName) {
|
|
160
|
+
// Task plan file missing — include with empty IO (will trigger ambiguous)
|
|
161
|
+
results.push({
|
|
162
|
+
id: taskEntry.id,
|
|
163
|
+
title: taskEntry.title,
|
|
164
|
+
inputFiles: [],
|
|
165
|
+
outputFiles: [],
|
|
166
|
+
done: taskEntry.done,
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const taskContent = await loadFile(join(tDir, taskFileName));
|
|
171
|
+
if (!taskContent) {
|
|
172
|
+
results.push({
|
|
173
|
+
id: taskEntry.id,
|
|
174
|
+
title: taskEntry.title,
|
|
175
|
+
inputFiles: [],
|
|
176
|
+
outputFiles: [],
|
|
177
|
+
done: taskEntry.done,
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const io = parseTaskPlanIO(taskContent);
|
|
182
|
+
results.push({
|
|
183
|
+
id: taskEntry.id,
|
|
184
|
+
title: taskEntry.title,
|
|
185
|
+
inputFiles: io.inputFiles,
|
|
186
|
+
outputFiles: io.outputFiles,
|
|
187
|
+
done: taskEntry.done,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return results;
|
|
191
|
+
}
|
|
192
|
+
// ─── State Persistence ────────────────────────────────────────────────────
|
|
193
|
+
function reactiveStatePath(basePath, mid, sid) {
|
|
194
|
+
return join(basePath, ".gsd", "runtime", `${mid}-${sid}-reactive.json`);
|
|
195
|
+
}
|
|
196
|
+
function isReactiveState(data) {
|
|
197
|
+
if (!data || typeof data !== "object")
|
|
198
|
+
return false;
|
|
199
|
+
const d = data;
|
|
200
|
+
return typeof d.sliceId === "string" && Array.isArray(d.completed);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Load persisted reactive execution state for a slice.
|
|
204
|
+
* Returns null when no state file exists or the file is invalid.
|
|
205
|
+
*/
|
|
206
|
+
export function loadReactiveState(basePath, mid, sid) {
|
|
207
|
+
return loadJsonFileOrNull(reactiveStatePath(basePath, mid, sid), isReactiveState);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Save reactive execution state to disk.
|
|
211
|
+
*/
|
|
212
|
+
export function saveReactiveState(basePath, mid, sid, state) {
|
|
213
|
+
saveJsonFile(reactiveStatePath(basePath, mid, sid), state);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Remove the reactive state file when a slice completes.
|
|
217
|
+
*/
|
|
218
|
+
export function clearReactiveState(basePath, mid, sid) {
|
|
219
|
+
const path = reactiveStatePath(basePath, mid, sid);
|
|
220
|
+
try {
|
|
221
|
+
if (existsSync(path))
|
|
222
|
+
unlinkSync(path);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Non-fatal
|
|
226
|
+
}
|
|
227
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gsd-pi",
|
|
3
|
-
"version": "2.37.1-dev.
|
|
3
|
+
"version": "2.37.1-dev.49503be",
|
|
4
4
|
"description": "GSD — Get Shit Done coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@anthropic-ai/sdk": "^0.73.0",
|
|
87
|
+
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
|
87
88
|
"@aws-sdk/client-bedrock-runtime": "^3.983.0",
|
|
88
89
|
"@clack/prompts": "^1.1.0",
|
|
89
90
|
"@google/genai": "^1.40.0",
|
|
@@ -53,6 +53,19 @@ export function getEnvApiKey(provider) {
|
|
|
53
53
|
if (provider === "anthropic") {
|
|
54
54
|
return process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
|
|
55
55
|
}
|
|
56
|
+
// Anthropic on Vertex AI uses Application Default Credentials.
|
|
57
|
+
// Detected via ANTHROPIC_VERTEX_PROJECT_ID (same env var as Claude Code).
|
|
58
|
+
if (provider === "anthropic-vertex") {
|
|
59
|
+
const hasProject = !!process.env.ANTHROPIC_VERTEX_PROJECT_ID;
|
|
60
|
+
if (hasProject) {
|
|
61
|
+
return "<authenticated>";
|
|
62
|
+
}
|
|
63
|
+
// Fall back to Google Cloud project env vars
|
|
64
|
+
const hasGoogleProject = !!(process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT);
|
|
65
|
+
if (hasGoogleProject && hasVertexAdcCredentials()) {
|
|
66
|
+
return "<authenticated>";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
56
69
|
// Vertex AI uses Application Default Credentials, not API keys.
|
|
57
70
|
// Auth is configured via `gcloud auth application-default login`.
|
|
58
71
|
if (provider === "google-vertex") {
|