diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index 85bdbe370..e3d13d9b5 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -610,6 +610,25 @@ export async function bootstrapAutoSession( }; } + // Apply worker model override from parallel orchestrator (#worker-model). + // GSD_WORKER_MODEL is injected by the coordinator when parallel.worker_model + // is configured, so parallel milestone workers use a cheaper model than the + // coordinator session (e.g. Haiku for execution, Sonnet for planning). + const workerModelOverride = process.env.GSD_WORKER_MODEL; + if (workerModelOverride && process.env.GSD_PARALLEL_WORKER === "1") { + const availableModels = ctx.modelRegistry.getAvailable(); + const { resolveModelId } = await import("./auto-model-selection.js"); + const overrideModel = resolveModelId(workerModelOverride, availableModels, ctx.model?.provider); + if (overrideModel) { + const ok = await pi.setModel(overrideModel, { persist: false }); + if (ok) { + // Update start model so all subsequent units use this as the baseline + s.autoModeStartModel = { provider: overrideModel.provider, id: overrideModel.id }; + ctx.ui.notify(`Worker model override: ${overrideModel.provider}/${overrideModel.id}`, "info"); + } + } + } + // Snapshot installed skills if (resolveSkillDiscoveryMode() !== "off") { snapshotSkills(); diff --git a/src/resources/extensions/gsd/docs/preferences-reference.md b/src/resources/extensions/gsd/docs/preferences-reference.md index 8f110ce37..9d3d29183 100644 --- a/src/resources/extensions/gsd/docs/preferences-reference.md +++ b/src/resources/extensions/gsd/docs/preferences-reference.md @@ -204,6 +204,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea - `budget_ceiling`: number — optional per-parallel-run budget ceiling. - `merge_strategy`: `"per-slice"` or `"per-milestone"` — when to merge worktree results back. Default: `"per-milestone"`. - `auto_merge`: `"auto"`, `"confirm"`, or `"manual"` — merge behavior after completion. `"auto"` merges immediately; `"confirm"` asks first; `"manual"` leaves branches for you. Default: `"confirm"`. + - `worker_model`: string — optional model override for parallel milestone workers. When set, workers use this model (e.g. `"claude-haiku-4-5"`) instead of inheriting the coordinator's model. Useful for cost savings on execution-heavy milestones. - `verification_commands`: string[] — shell commands to run as verification after task execution (e.g., `["npm test", "npm run lint"]`). Commands run in order; if any fails, the task is marked as needing fixes. diff --git a/src/resources/extensions/gsd/parallel-orchestrator.ts b/src/resources/extensions/gsd/parallel-orchestrator.ts index 95b87d738..dda0fae5c 100644 --- a/src/resources/extensions/gsd/parallel-orchestrator.ts +++ b/src/resources/extensions/gsd/parallel-orchestrator.ts @@ -548,19 +548,27 @@ export function spawnWorker( let child: ChildProcess; try { + const workerEnv: Record = { + ...process.env, + GSD_MILESTONE_LOCK: milestoneId, + // Pass the real project root so workers don't need to re-derive it. + // Without this, process.cwd() resolves symlinks and the worktree + // path heuristic can match the user-level ~/.gsd instead of the + // project .gsd, causing writes to ~ and corrupting user config. + GSD_PROJECT_ROOT: basePath, + // Prevent workers from spawning their own parallel sessions + GSD_PARALLEL_WORKER: "1", + }; + + // Apply worker model override if configured, so workers use a cheaper + // model (e.g. Haiku) rather than inheriting the coordinator's model. + if (state.config.worker_model) { + workerEnv.GSD_WORKER_MODEL = state.config.worker_model; + } + child = spawn(process.execPath, [binPath, "headless", "--json", "auto"], { cwd: worker.worktreePath, - env: { - ...process.env, - GSD_MILESTONE_LOCK: milestoneId, - // Pass the real project root so workers don't need to re-derive it. - // Without this, process.cwd() resolves symlinks and the worktree - // path heuristic can match the user-level ~/.gsd instead of the - // project .gsd, causing writes to ~ and corrupting user config. - GSD_PROJECT_ROOT: basePath, - // Prevent workers from spawning their own parallel sessions - GSD_PARALLEL_WORKER: "1", - }, + env: workerEnv, stdio: ["ignore", "pipe", "pipe"], detached: false, }); diff --git a/src/resources/extensions/gsd/preferences-validation.ts b/src/resources/extensions/gsd/preferences-validation.ts index 6b4e0e217..f48113c48 100644 --- a/src/resources/extensions/gsd/preferences-validation.ts +++ b/src/resources/extensions/gsd/preferences-validation.ts @@ -492,6 +492,14 @@ export function validatePreferences(preferences: GSDPreferences): { } } + if (p.worker_model !== undefined) { + if (typeof p.worker_model === "string" && p.worker_model.length > 0) { + parallel.worker_model = p.worker_model; + } else { + errors.push("parallel.worker_model must be a non-empty string"); + } + } + if (Object.keys(parallel).length > 0) { validated.parallel = parallel as unknown as import("./types.js").ParallelConfig; } diff --git a/src/resources/extensions/gsd/preferences.ts b/src/resources/extensions/gsd/preferences.ts index 58badbd95..55d0be8a3 100644 --- a/src/resources/extensions/gsd/preferences.ts +++ b/src/resources/extensions/gsd/preferences.ts @@ -537,5 +537,6 @@ export function resolveParallelConfig(prefs: GSDPreferences | undefined): import budget_ceiling: prefs?.parallel?.budget_ceiling, merge_strategy: prefs?.parallel?.merge_strategy ?? "per-milestone", auto_merge: prefs?.parallel?.auto_merge ?? "confirm", + worker_model: prefs?.parallel?.worker_model, }; } diff --git a/src/resources/extensions/gsd/types.ts b/src/resources/extensions/gsd/types.ts index ffecfc75e..e331c7df0 100644 --- a/src/resources/extensions/gsd/types.ts +++ b/src/resources/extensions/gsd/types.ts @@ -450,6 +450,8 @@ export interface ParallelConfig { budget_ceiling?: number; merge_strategy: MergeStrategy; auto_merge: AutoMergeMode; + /** Optional model override for parallel milestone workers (e.g. "claude-haiku-4-5"). */ + worker_model?: string; } // ─── Reactive Task Execution Types ───────────────────────────────────────