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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-17
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# agent-claude-cap-cargo-jobs-in-agent-launch-env-2026-05-17-21-02 (minimal / T1)

Branch: `agent/claude/cap-cargo-jobs-in-agent-launch-env-2026-05-17-21-02`

Cap `CARGO_BUILD_JOBS` in the env prefix of every agent launch command so concurrent agent runs don't oversubscribe the host when child cargo builds fan out. Formula: `max(2, floor(os.cpus().length / 4))`. Harmless on non-Rust agents (env var is only read by cargo).

## Files

- `src/agents/launch.js` — new `buildResourceEnv()`, prepended to `buildSessionEnv()` in `buildAgentLaunchCommand`.
- `test/agents-launch.test.js` — expected `CARGO_BUILD_JOBS=<n>` is computed from `os.cpus()` (was hardcoded `=8`, only passed on 32-CPU hosts). Added a coverage test asserting the formula.
- `test/agents-start-dry-run.test.js` — regex relaxed to `CARGO_BUILD_JOBS=\d+`.
- `test/agents-start.test.js` — canonical-session `launchCommand` assertions updated to use the computed `CARGO` prefix.

## Verification

- `node --test test/agents-launch.test.js test/agents-start-dry-run.test.js test/agents-start.test.js` — 24/24 pass.
- `npm test` — 570 pass / 23 fail; failure count identical to clean `main` baseline (pre-existing, unrelated).

## Handoff

- Handoff: change=`agent-claude-cap-cargo-jobs-in-agent-launch-env-2026-05-17-21-02`; branch=`agent/claude/cap-cargo-jobs-in-agent-launch-env-2026-05-17-21-02`; scope=`cap CARGO_BUILD_JOBS in agent launch env`; action=`finish cleanup`.

## Cleanup

- [ ] Run: `gx branch finish --branch agent/claude/cap-cargo-jobs-in-agent-launch-env-2026-05-17-21-02 --base main --via-pr --wait-for-merge --cleanup`
- [ ] Record PR URL + `MERGED` state in the completion handoff.
- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`).
11 changes: 10 additions & 1 deletion src/agents/launch.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ function buildPromptCommand(parts, agent, prompt) {
return commandToShell([...parts, prompt]);
}

function buildResourceEnv() {
const cpus = require('node:os').cpus().length || 8;
// Cap each agent's cargo parallelism to avoid overwhelming the system
// when multiple agents build concurrently.
const jobs = Math.max(2, Math.floor(cpus / 4));
return [`CARGO_BUILD_JOBS=${jobs}`];
}

function buildAgentLaunchCommand(options) {
if (!options || typeof options !== 'object') {
throw new TypeError('options are required');
Expand All @@ -207,7 +215,8 @@ function buildAgentLaunchCommand(options) {
}

const launchCommand = buildPromptCommand(baseParts, agent, prompt);
const envPrefix = buildSessionEnv(agent, sessionId).join(' ');
const envParts = [...buildResourceEnv(), ...buildSessionEnv(agent, sessionId)];
const envPrefix = envParts.join(' ');
const launchWithEnv = envPrefix ? `${envPrefix} ${launchCommand}` : launchCommand;
if (!worktreePath) return launchWithEnv;
return `cd ${shellQuote(worktreePath)} && ${launchWithEnv}`;
Expand Down
31 changes: 22 additions & 9 deletions test/agents-launch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

const assert = require('node:assert/strict');
const test = require('node:test');
const os = require('node:os');

const {
buildAgentLaunchCommand,
buildAgentResumeCommand,
} = require('../src/agents/launch');

const JOBS = Math.max(2, Math.floor((os.cpus().length || 8) / 4));
const CARGO = `CARGO_BUILD_JOBS=${JOBS}`;

test('builds codex launch commands with positional prompts', () => {
assert.equal(
buildAgentLaunchCommand({
Expand All @@ -17,7 +21,7 @@ test('builds codex launch commands with positional prompts', () => {
permissionMode: 'workspace-write',
sessionId: 'session-1',
}),
"cd '/tmp/work tree' && OMX_SESSION_ID='session-1' 'codex' '--permission-mode' 'workspace-write' 'fix tests'",
`cd '/tmp/work tree' && ${CARGO} OMX_SESSION_ID='session-1' 'codex' '--permission-mode' 'workspace-write' 'fix tests'`,
);
});

Expand All @@ -28,7 +32,7 @@ test('builds claude launch commands with argument prompts', () => {
prompt: 'review code',
permissionMode: 'acceptEdits',
}),
"'claude' '--permission-mode' 'acceptEdits' 'review code'",
`${CARGO} 'claude' '--permission-mode' 'acceptEdits' 'review code'`,
);
});

Expand All @@ -38,7 +42,7 @@ test('builds opencode launch commands with positional prompts', () => {
agentId: 'opencode',
prompt: 'implement feature',
}),
"'opencode' 'implement feature'",
`${CARGO} 'opencode' 'implement feature'`,
);
});

Expand All @@ -49,7 +53,7 @@ test('builds cursor launch commands with argument prompts', () => {
prompt: 'inspect current branch',
worktreePath: '/repo/worktree',
}),
"cd '/repo/worktree' && 'cursor-agent' 'inspect current branch'",
`cd '/repo/worktree' && ${CARGO} 'cursor-agent' 'inspect current branch'`,
);
});

Expand All @@ -60,7 +64,7 @@ test('builds gemini launch commands with argument prompts', () => {
prompt: 'summarize repo',
sessionId: 'session-2',
}),
"OMX_SESSION_ID='session-2' 'gemini' 'summarize repo'",
`${CARGO} OMX_SESSION_ID='session-2' 'gemini' 'summarize repo'`,
);
});

Expand All @@ -69,27 +73,36 @@ test('quotes prompts with single quotes, newlines, and dollar signs safely', ()

assert.equal(
buildAgentLaunchCommand({ agentId: 'codex', prompt }),
"'codex' 'say '\\''hello'\\''\nthen echo $HOME'",
`${CARGO} 'codex' 'say '\\''hello'\\''\nthen echo $HOME'`,
);

assert.equal(
buildAgentLaunchCommand({ agentId: 'gemini', prompt }),
"'gemini' 'say '\\''hello'\\''\nthen echo $HOME'",
`${CARGO} 'gemini' 'say '\\''hello'\\''\nthen echo $HOME'`,
);

assert.equal(
buildAgentLaunchCommand({ agentId: 'cursor', prompt }),
"'cursor-agent' 'say '\\''hello'\\''\nthen echo $HOME'",
`${CARGO} 'cursor-agent' 'say '\\''hello'\\''\nthen echo $HOME'`,
);
});

test('omits prompts when none are supplied', () => {
assert.equal(
buildAgentLaunchCommand({ agentId: 'codex', worktreePath: '/repo' }),
"cd '/repo' && 'codex'",
`cd '/repo' && ${CARGO} 'codex'`,
);
});

test('caps CARGO_BUILD_JOBS at floor(cpus/4), minimum 2', () => {
const cmd = buildAgentLaunchCommand({ agentId: 'codex', prompt: 'x' });
const match = cmd.match(/CARGO_BUILD_JOBS=(\d+)/);
assert.ok(match, 'CARGO_BUILD_JOBS env var must be present');
const jobs = Number(match[1]);
assert.ok(jobs >= 2, `expected jobs >= 2, got ${jobs}`);
assert.equal(jobs, JOBS);
});

test('builds resume commands for supported agents', () => {
assert.equal(buildAgentResumeCommand('codex', 'workspace-write'), "'codex' 'resume' '--permission-mode' 'workspace-write'");
assert.equal(buildAgentResumeCommand('claude'), "'claude' '--continue'");
Expand Down
8 changes: 4 additions & 4 deletions test/agents-start-dry-run.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ test('gx agents start dry-run prints the planned codex branch, worktree, and lau
result.stdout,
new RegExp(`worktree: ${repoDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/\\.omx/agent-worktrees/[^\\n]*codex__fix-auth-tests-2026-04-29-21-30`),
);
assert.match(result.stdout, /launch: cd '.*' && 'codex' 'fix auth tests'/);
assert.match(result.stdout, /launch: cd '.*' && CARGO_BUILD_JOBS=\d+ 'codex' 'fix auth tests'/);
assert.match(result.stdout, /No branch, worktree, session metadata, or agent process was created\./);

const branchCheck = runCmd(
Expand Down Expand Up @@ -64,7 +64,7 @@ test('gx agents start dry-run supports claude worktree planning and rejects unkn
assert.equal(claudeResult.status, 0, claudeResult.stderr || claudeResult.stdout);
assert.match(claudeResult.stdout, /branch: agent\/claude\/update-docs-2026-04-29-21-31/);
assert.match(claudeResult.stdout, /\.omc\/agent-worktrees\/[^ \n]*claude__update-docs-2026-04-29-21-31/);
assert.match(claudeResult.stdout, /launch: cd '.*' && 'claude' 'update docs'/);
assert.match(claudeResult.stdout, /launch: cd '.*' && CARGO_BUILD_JOBS=\d+ 'claude' 'update docs'/);

const invalidResult = runNode(
['agents', 'start', 'update docs', '--agent', 'bogus', '--dry-run'],
Expand Down Expand Up @@ -164,7 +164,7 @@ test('gx agents start --dry-run --json emits Colony-ready launch plan', () => {
assert.equal(payload.branch, 'agent/codex/colony-dry-run-2026-04-30-00-05');
assert.match(payload.worktree, /\.omx\/agent-worktrees\/repo__codex__colony-dry-run-2026-04-30-00-05$/);
assert.deepEqual(payload.claimedFiles, ['README.md']);
assert.match(payload.launchCommand, /cd '.*' && 'codex' 'colony dry run'/);
assert.match(payload.launchCommand, /cd '.*' && CARGO_BUILD_JOBS=\d+ 'codex' 'colony dry run'/);
assert.equal(payload.tmuxSession, null);
assert.equal(payload.tmuxTarget, null);
assert.deepEqual(payload.metadata, {
Expand Down Expand Up @@ -300,5 +300,5 @@ test('interactive launcher panel asks for a task when opened empty', () => {
const output = stdout.chunks.join('');
assert.match(output, /task: fix auth/);
assert.match(output, /branch: agent\/codex\/fix-auth-/);
assert.match(output, /launch: cd '.*' && 'codex' 'fix auth'/);
assert.match(output, /launch: cd '.*' && CARGO_BUILD_JOBS=\d+ 'codex' 'fix auth'/);
});
7 changes: 5 additions & 2 deletions test/agents-start.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');

const CARGO_JOBS = Math.max(2, Math.floor((os.cpus().length || 8) / 4));
const CARGO = `CARGO_BUILD_JOBS=${CARGO_JOBS}`;

function loadStartWithMocks({
runPackageAsset,
createAgentSession,
Expand Down Expand Up @@ -95,7 +98,7 @@ test('agents start creates canonical session after successful branch start', ()
base: 'main',
claims: [],
metadata: {},
launchCommand: "cd '/repo/.omx/agent-worktrees/repo__codex__fix-auth' && 'codex' 'fix auth'",
launchCommand: `cd '/repo/.omx/agent-worktrees/repo__codex__fix-auth' && ${CARGO} 'codex' 'fix auth'`,
tmux: null,
status: 'active',
},
Expand Down Expand Up @@ -183,7 +186,7 @@ test('agents start claim failure updates canonical session to claim-failed', ()
base: 'main',
claims: ['src/auth.js'],
metadata: {},
launchCommand: "cd '/repo/.omx/agent-worktrees/repo__codex__fix-auth' && 'codex' 'fix auth'",
launchCommand: `cd '/repo/.omx/agent-worktrees/repo__codex__fix-auth' && ${CARGO} 'codex' 'fix auth'`,
tmux: null,
status: 'claim-failed',
claimFailure: {
Expand Down
Loading