Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/main/hive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')}/<id>.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-<id>\`, 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')}/<id>.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-<id>\`, 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.'
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 18 additions & 4 deletions src/main/workerLaunch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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}`;
Expand Down
21 changes: 21 additions & 0 deletions test/worker-launch.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading