diff --git a/src/resources/extensions/gsd/preferences-validation.ts b/src/resources/extensions/gsd/preferences-validation.ts index bee6b810f..ef80ef1d6 100644 --- a/src/resources/extensions/gsd/preferences-validation.ts +++ b/src/resources/extensions/gsd/preferences-validation.ts @@ -569,7 +569,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`); diff --git a/src/resources/extensions/gsd/reactive-graph.ts b/src/resources/extensions/gsd/reactive-graph.ts index eb76999f6..dff1718df 100644 --- a/src/resources/extensions/gsd/reactive-graph.ts +++ b/src/resources/extensions/gsd/reactive-graph.ts @@ -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. diff --git a/src/resources/extensions/gsd/tests/reactive-graph.test.ts b/src/resources/extensions/gsd/tests/reactive-graph.test.ts index 4cf077056..8e16e28a5 100644 --- a/src/resources/extensions/gsd/tests/reactive-graph.test.ts +++ b/src/resources/extensions/gsd/tests/reactive-graph.test.ts @@ -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" }, + ]); +}); diff --git a/src/resources/extensions/gsd/types.ts b/src/resources/extensions/gsd/types.ts index b5a0c0b17..e03815520 100644 --- a/src/resources/extensions/gsd/types.ts +++ b/src/resources/extensions/gsd/types.ts @@ -481,6 +481,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. */