feat(reactive): graph diagnostics and subagent_model config

Add getMissingAnnotationTasks() to surface which tasks lack IO
annotations and prevent parallel dispatch. Also add subagent_model
to ReactiveExecutionConfig for overriding the model used by
subagents during parallel task execution.

- getMissingAnnotationTasks() with 4 tests
- subagent_model field on ReactiveExecutionConfig type
- Validation for reactive_execution.subagent_model preference
This commit is contained in:
Jeremy 2026-03-29 05:45:10 -05:00
parent 0a2c9b64c6
commit 3c7ec7a8da
4 changed files with 74 additions and 1 deletions

View file

@ -523,7 +523,15 @@ export function validatePreferences(preferences: GSDPreferences): {
}
}
const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode"]);
if (re.subagent_model !== undefined) {
if (typeof re.subagent_model === "string" && re.subagent_model.length > 0) {
validRe.subagent_model = re.subagent_model;
} else {
errors.push("reactive_execution.subagent_model must be a non-empty string");
}
}
const knownReKeys = new Set(["enabled", "max_parallel", "isolation_mode", "subagent_model"]);
for (const key of Object.keys(re)) {
if (!knownReKeys.has(key)) {
warnings.push(`unknown reactive_execution key "${key}" — ignored`);

View file

@ -131,6 +131,24 @@ export function isGraphAmbiguous(graph: DerivedTaskNode[]): boolean {
);
}
/**
* Returns tasks that are missing IO annotations (no inputFiles and no outputFiles).
* These tasks prevent parallel dispatch by making the graph ambiguous.
* Used to surface actionable diagnostics when parallel execution falls back to sequential.
*/
export function getMissingAnnotationTasks(
graph: DerivedTaskNode[],
): Array<{ id: string; title: string }> {
return graph
.filter(
(node) =>
!node.done &&
node.inputFiles.length === 0 &&
node.outputFiles.length === 0,
)
.map((node) => ({ id: node.id, title: node.title }));
}
/**
* Detect deadlock: no tasks are ready and none are in-flight, yet incomplete
* tasks remain. This indicates a circular dependency or impossible state.

View file

@ -5,6 +5,7 @@ import {
getReadyTasks,
chooseNonConflictingSubset,
isGraphAmbiguous,
getMissingAnnotationTasks,
detectDeadlock,
graphMetrics,
} from "../reactive-graph.ts";
@ -297,3 +298,47 @@ test("graphMetrics computes correct values", () => {
assert.equal(metrics.readySetSize, 2); // T02 (T01 done) and T03 (no deps)
assert.equal(metrics.ambiguous, false);
});
// ─── getMissingAnnotationTasks ─────────────────────────────────────────────
test("getMissingAnnotationTasks: returns empty array when all tasks have annotations", () => {
const graph: DerivedTaskNode[] = [
{ id: "T01", title: "A", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
{ id: "T02", title: "B", inputFiles: [], outputFiles: ["src/c.ts"], done: false, dependsOn: [] },
];
assert.deepEqual(getMissingAnnotationTasks(graph), []);
});
test("getMissingAnnotationTasks: returns tasks with missing annotations", () => {
const graph: DerivedTaskNode[] = [
{ id: "T01", title: "A", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
{ id: "T02", title: "B", inputFiles: ["src/a.ts"], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
{ id: "T03", title: "C", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
];
assert.deepEqual(getMissingAnnotationTasks(graph), [
{ id: "T01", title: "A" },
{ id: "T03", title: "C" },
]);
});
test("getMissingAnnotationTasks: skips done tasks", () => {
const graph: DerivedTaskNode[] = [
{ id: "T01", title: "A", inputFiles: [], outputFiles: [], done: true, dependsOn: [] },
{ id: "T02", title: "B", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
];
assert.deepEqual(getMissingAnnotationTasks(graph), [
{ id: "T02", title: "B" },
]);
});
test("getMissingAnnotationTasks: returns only tasks missing BOTH inputFiles and outputFiles", () => {
const graph: DerivedTaskNode[] = [
{ id: "T01", title: "InputOnly", inputFiles: ["src/a.ts"], outputFiles: [], done: false, dependsOn: [] },
{ id: "T02", title: "OutputOnly", inputFiles: [], outputFiles: ["src/b.ts"], done: false, dependsOn: [] },
{ id: "T03", title: "Neither", inputFiles: [], outputFiles: [], done: false, dependsOn: [] },
{ id: "T04", title: "Both", inputFiles: ["src/c.ts"], outputFiles: ["src/d.ts"], done: false, dependsOn: [] },
];
assert.deepEqual(getMissingAnnotationTasks(graph), [
{ id: "T03", title: "Neither" },
]);
});

View file

@ -476,6 +476,8 @@ export interface ReactiveExecutionConfig {
max_parallel: number;
/** Isolation mode for parallel tasks within a slice. Currently only "same-tree" is supported. */
isolation_mode: "same-tree";
/** Optional model override for subagents spawned during parallel execution. */
subagent_model?: string;
}
/** Per-slice reactive execution runtime state, persisted to disk. */