From 961801e203cd3989b2ed000c8eb22463fe943e34 Mon Sep 17 00:00:00 2001 From: hsien wei Date: Wed, 9 Sep 2026 23:13:53 +0800 Subject: [PATCH] fix(worker): resolve default command from requested provider - derive omitted worker commands from explicit provider presets - preserve explicit command precedence and custom-provider fallbacks - document provider and command precedence in the generated hive protocol - add regressions for provider-only, custom, and invalid-provider launches --- src/main/hive.ts | 6 +++--- src/main/workerLaunch.ts | 22 ++++++++++++++++++---- test/worker-launch.test.cjs | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/main/hive.ts b/src/main/hive.ts index 4537c2bf6..6020d1188 100644 --- a/src/main/hive.ts +++ b/src/main/hive.ts @@ -1481,7 +1481,7 @@ export class HiveManager { // saying nothing, and COMMANDS.md documents it either way for the case where // the operator turns it on after god was already running. const spawnQueueLine = meta.isGod && this.orchestratorMaySpawn() - ? `SPAWNING A WORKER: you can start an ephemeral worker yourself by writing ONE JSON file into ${inRoot('spawn-requests')}/.json. Required: \`objective\` (what the worker must do) and \`cwd\` (the repo it runs in). Optional: \`name\`, \`command\`, \`provider\`, \`model\`, \`isolate\` (default true = its own git worktree), \`tokenCap\`, and \`slack\` ({channel, thread_ts}) to route its failures back to a thread. The harness polls that directory, spawns \`worker-\`, and moves the request to \`spawn-requests/.done/\` on success or \`.failed/\` with a reason. This is the ONLY way you can spawn; a hire manifest under research/hires/ needs the human to confirm it in the UI, so it is not a route you can complete on your own. Reuse an existing agent first, as above — a worker is a fresh spend every time.` + ? `SPAWNING A WORKER: you can start an ephemeral worker yourself by writing ONE JSON file into ${inRoot('spawn-requests')}/.json. Required: \`objective\` (what the worker must do) and \`cwd\` (the repo it runs in). Optional: \`name\`, \`command\` (overrides the provider default), \`provider\` (selects its default CLI when command is omitted), \`model\`, \`isolate\` (default true = its own git worktree), \`tokenCap\`, and \`slack\` ({channel, thread_ts}) to route its failures back to a thread. The harness polls that directory, spawns \`worker-\`, and moves the request to \`spawn-requests/.done/\` on success or \`.failed/\` with a reason. This is the ONLY way you can spawn; a hire manifest under research/hires/ needs the human to confirm it in the UI, so it is not a route you can complete on your own. Reuse an existing agent first, as above — a worker is a fresh spend every time.` : ''; const godLine = meta.isGod ? 'You are the GOD / ORCHESTRATOR of this hive — your job is to ORCHESTRATE, not to implement: maintain live situational awareness and delegate the work. (1) AWARENESS — always know what is going on: keep an accurate picture of every agent (active vs archived/idle), the task board, and all in-flight work; drain your inbox continually and triage every other agent\'s requests, answering clarifications so the team runs autonomously. (2) DELEGATE — decompose work and fan it out to the hive agents via their inboxes (route messages and assign owners; do not do their jobs); do NOT take on grunt implementation yourself. Stay aware of who is already on the floor and delegate OPPORTUNISTICALLY: BEFORE you spawn anything, CHECK THE LIVE ROSTER (active agents in registry.json + their state in fleet.json) and prefer routing to an EXISTING agent that fits — above all when the request names one ("ask Pam to…", "have Jim…"), route to that agent instead of reflexively creating a new one. Reuse an idle or already-running agent whose role matches; only spawn a fresh agent when no existing one is a sensible fit, and say that you checked. One capable owner beats a duplicate. (3) OWN ONLY THE IMPORTANT, high-leverage things — task decomposition, dispatch decisions, sign-offs, conflict resolution, branch integration, and final QA — and remain the sole scribe of board.md. You are otherwise fully autonomous — there is NO separate approval queue. For the genuinely critical (destructive actions, spending real money, scope changes, unresolvable conflicts), ask the human directly in your own session and let the tool-permission prompt gate the action; the human approves natively, including remotely from their phone via /remote-control. Keep the team unblocked. When you DISPATCH a task, write it as a 4-part contract so the agent can run autonomously: (1) OBJECTIVE — the concrete goal; (2) OUTPUT — the expected deliverable/format; (3) TOOLS — what to use or avoid, and any references to read instead of re-deriving; (4) BOUNDARIES — scope limits + the definition of done. Pass references (file paths, message ids, board sections), not pasted content — keep dispatches short.' @@ -2910,8 +2910,8 @@ the hive root: "objective": "what the worker must do (required)", "cwd": "/absolute/path/to/the/repo (required)", "name": "display name (optional)", - "command": "engine CLI (optional; defaults to the configured one)", - "provider": "claude | codex | cursor | antigravity | … (optional)", + "command": "engine CLI (optional; overrides the provider default)", + "provider": "claude | codex | cursor | antigravity | … (optional; selects its default CLI when command is omitted)", "model": "model override (optional)", "isolate": true, "tokenCap": 0, diff --git a/src/main/workerLaunch.ts b/src/main/workerLaunch.ts index 94e8b5bad..fc12f6b76 100644 --- a/src/main/workerLaunch.ts +++ b/src/main/workerLaunch.ts @@ -3,7 +3,13 @@ * pure function: this exact translation silently killed real workers for days * while reporting success, which is what earned it a unit test. */ -import { autoModeFlagForProvider, hasAutoModeStance, inferAgentProvider } from '../shared/agentProvider'; +import { + autoModeFlagForProvider, + defaultCommandForProvider, + hasAutoModeStance, + inferAgentProvider, + normalizeAgentProvider +} from '../shared/agentProvider'; import { tokenizeCommand } from '../shared/commandLine'; export interface WorkerLaunch { @@ -25,10 +31,18 @@ export function buildWorkerLaunch(opts: { /** The app's auto (skip-permissions) setting. */ autoMode: boolean; }): WorkerLaunch { - let command = + const requestCommand = typeof opts.requestCommand === 'string' && opts.requestCommand.trim() ? opts.requestCommand.trim() - : (opts.defaultCommand ?? 'claude'); + : ''; + const requestProvider = normalizeAgentProvider(opts.requestProvider); + const fallbackCommand = opts.defaultCommand ?? 'claude'; + // An explicit command may be a wrapper or shim and remains authoritative. + // Without one, keep the executable and provider behavior coherent by taking + // the provider's canonical command before the configured legacy fallback. + let command = + requestCommand || + (requestProvider ? defaultCommandForProvider(requestProvider, fallbackCommand) : fallbackCommand); // Inherit the app's auto (skip-permissions) mode when the request takes no // stance of its own: a headless worker has no human to click through tool // prompts, so without the flag it stalls at the first ask until the idle @@ -38,7 +52,7 @@ export function buildWorkerLaunch(opts: { // non-claude worker stalling; review caught it). An explicit stance in the // request still wins: the flag's leading token already present as a TOKEN // (not substring — copilot's flag starts with `-s`) means the request chose. - const provider = inferAgentProvider(command, opts.requestProvider); + const provider = inferAgentProvider(command, requestProvider); const autoFlag = opts.autoMode ? autoModeFlagForProvider(provider) : ''; if (autoFlag && !hasAutoModeStance(tokenizeCommand(command), provider)) { command += ` ${autoFlag}`; diff --git a/test/worker-launch.test.cjs b/test/worker-launch.test.cjs index 4fbd794c3..4712914e7 100644 --- a/test/worker-launch.test.cjs +++ b/test/worker-launch.test.cjs @@ -91,9 +91,30 @@ test('a multi-token auto flag appends whole, and the stance check is by token', test("an explicit request provider picks that provider's flag for a custom binary", () => { const l = launch({ requestCommand: 'my-codex-wrapper', requestProvider: 'codex', autoMode: true }); + assert.equal(l.bin, 'my-codex-wrapper'); assert.deepEqual(l.args, ['-a', 'never', '-s', 'workspace-write']); }); +test('a missing command resolves from an explicit provider before the configured default', () => { + const l = launch({ requestProvider: 'codex', defaultCommand: 'claude', autoMode: true }); + assert.equal(l.bin, 'codex'); + assert.deepEqual(l.args, ['-a', 'never', '-s', 'workspace-write']); +}); + +test('provider-only resolution honors providers whose auto-mode stance is config-based', () => { + const l = launch({ requestProvider: 'opencode', defaultCommand: 'claude', autoMode: true }); + assert.equal(l.bin, 'opencode'); + assert.deepEqual(l.args, []); +}); + +test('custom and invalid providers preserve the configured command fallback', () => { + const custom = launch({ requestProvider: 'custom', defaultCommand: 'company-agent', autoMode: true }); + assert.equal(custom.bin, 'company-agent'); + assert.deepEqual(custom.args, []); + + assert.equal(launch({ requestProvider: null, defaultCommand: 'opencode' }).bin, 'opencode'); +}); + test('a missing command falls back to the default, then to claude', () => { assert.equal(launch({ defaultCommand: 'codex --full-auto' }).bin, 'codex'); assert.equal(launch({}).bin, 'claude');