openvibers/openvibe.codes Commits

Effort on every run path; no hardcoded level, auto means the default (R1 item 1) (#378)

OP OpenVibers committed 3 days ago · 8937a4db7b · 19 files, +130 −23

* Effort on every run path; no hardcoded level, auto means the default (R1 item 1)

The task-start API, the task CLI and conductor sub-tasks accept effort now
(auto/low/medium/high/xhigh/max); the job input keeps auto too. The engine
adapters no longer fall back to a hardcoded medium (Codex) or high
(Command Code): with no effort, or auto, the CLIs take no flag and use
their own default, while the OpenVibe.AI path passes auto to AI, which
resolves it from its role and task defaults. A retry from an unknown level
steps up from medium instead of the bottom of the ladder.

* Merge remote-tracking branch 'origin/main' into codes-r1-effort

* A conductor sub-task takes the effort it is given (R1 item 1 test)

The sub-task start route accepts effort now; this covers it the way the
task-start route is covered: high passes through to the job, auto passes
through, and a level that is none is 422 codes.effort_invalid.

Changes

ddiff --git a/bin/mcp.js b/bin/mcp.js
iindex 3f2dd3d..19e1a77 100644
---- a/bin/mcp.js
++++ b/bin/mcp.js
@@@ -70,6 +70,7 @@ const TOOLS = [
agent: { type: 'string', description: 'codex, opencode, command-code, deepseek, aider: never claude-code or claude-other' },
model: { type: 'string', description: 'The model for that agent (never an Anthropic one)' },
ref: { type: 'string', description: 'A branch or tag to start from' },
++ effort: { type: 'string', description: 'How hard it thinks: auto, low, medium, high, xhigh or max' },
},
required: ['prompt', 'repo'],
additionalProperties: true,
@@@ -135,13 +136,15 @@ async function startTask(args) {
if (agent && /^(claude-code|claude-other|claude|cm)$/i.test(agent)) return failed('the conductor may start sub-tasks on any agent except an Anthropic one: that session is the conductor itself');
if (model && /(^|[^a-z])(claude|opus|sonnet|haiku|anthropic)([^a-z]|$)/i.test(model)) return failed('the conductor may start sub-tasks on any agent except an Anthropic model: that session is the conductor itself');
const c = auth();
++ const effort = typeof args.effort === 'string' && ['auto', 'low', 'medium', 'high', 'xhigh', 'max'].includes(args.effort) ? args.effort : null;
const answer = await call(c, 'POST', `/api/v1/conductor/${encodeURIComponent(task)}/tasks`, {
prompt, repo, ...(agent ? { agent } : {}), ...(model ? { model } : {}), ...(args.ref ? { ref: String(args.ref).slice(0, 100) } : {}),
++ ...(effort ? { effort } : {}),
});
const t = answer.task || {};
return text({
id: t.id, state: t.state, agent: t.agent, model: model || null, repo: t.repo, url: t.url,
-- ...(args.machine || args.effort ? { note: 'machine and effort are not fields of a Codes task yet: Run places it on one of the owner\'s machines, and the agent\'s own effort applies' } : {}),
++ ...(args.machine ? { note: 'machine is not a field of a Codes task yet: Run places it on one of the owner\'s machines' } : {}),
});
}
ddiff --git a/bin/openvibe-codes.js b/bin/openvibe-codes.js
iindex 3dfb13d..bcd4298 100755
---- a/bin/openvibe-codes.js
++++ b/bin/openvibe-codes.js
@@@ -27,8 +27,9 @@ const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your term
--agent <id> skip routing: claude-code, claude-other, codex, opencode, command-code,
aider, minimax-code, deepseek, openai-compatible
--model <name> the first agent's model
-- --effort <level> reasoning effort for agents that take one (low|medium|high|max);
-- the OpenVibe.AI agent passes it to AI's run
++ --effort <level> reasoning effort for agents that take one (auto|low|medium|high|max);
++ auto is each agent's own default; OpenVibe.AI resolves it from its
++ role and task defaults, the CLIs take no flag
--advisor let the OpenVibe.AI agent ask a stronger model for advice
mid-run (at most 3 uses; AI puts each use on the run's receipt)
--advisor-model <provider/model> the advisor model (default: AI's reviewer role, then its strongest)
ddiff --git a/bin/task.js b/bin/task.js
iindex 89fa953..69b825c 100644
---- a/bin/task.js
++++ b/bin/task.js
@@@ -6,10 +6,10 @@
* this machine: a task runs on one of your machines through OpenVibe.Run, and this client only talks to
* https://openvibe.codes/api/v1/tasks (server/http/tasks-api.js).
*
-- * openvibe-codes task start --repo owner/repo --file brief.md [--agent …] [--name …] [--tier …]
++ * openvibe-codes task start --repo owner/repo --file brief.md [--agent …] [--name …] [--tier …] [--effort …]
* openvibe-codes task list [--state running|needs|done]
* openvibe-codes task show <id> [--follow]
-- * openvibe-codes task continue <id> [--note "…"] [--agent …]
++ * openvibe-codes task continue <id> [--note "…"] [--agent …] [--effort …]
* openvibe-codes task stop <id>
*
* Who you are: a personal Network access token, from OPENVIBE_TOKEN or a file (~/.config/openvibe/token, mode 600),
@@@ -44,10 +44,11 @@ const USAGE = `openvibe-codes task — coding tasks on OpenVibe.Codes, as you
--file <path|-> the brief ("-" reads it from stdin)
--name <n> a name for the task: starting it twice with one name answers the first task
--tier basic|standard|hard the brief's tier (the engine forces it, as "tier: …" in the brief)
++ --effort auto|low|medium|high|xhigh|max how hard it thinks (default: the agent's own)
--permission review|edit|full review: read-only (research, an audit, a review); edit (default); full
openvibe-codes task list [--state running|needs|done] [--limit 20]
openvibe-codes task show <id> [--follow] --follow streams the task's events until its outcome
-- openvibe-codes task continue <id> [--note "…"] [--agent <agent>]
++ openvibe-codes task continue <id> [--note "…"] [--agent <agent>] [--effort <level>]
openvibe-codes task stop <id>
--json prints the API's answer as JSON instead of a readable log.
@@@ -70,7 +71,7 @@ function parse(argv) {
else if (a === '--help' || a === '-h') o.help = true;
else if (a.startsWith('--')) {
const k = a.slice(2);
-- if (!['repo', 'agent', 'machine', 'file', 'name', 'tier', 'state', 'permission', 'note', 'limit'].includes(k)) throw new UsageError(`unknown option --${k}`);
++ if (!['repo', 'agent', 'machine', 'file', 'name', 'tier', 'state', 'permission', 'note', 'limit', 'effort'].includes(k)) throw new UsageError(`unknown option --${k}`);
const v = argv[++i];
if (v === undefined || v.startsWith('--')) throw new UsageError(`--${k} needs a value`);
o[k] = v;
@@@ -261,6 +262,10 @@ async function cmdStart(o, c) {
if (!/^[A-Za-z0-9_.:-]{4,120}$/.test(o.name)) throw new UsageError('--name: 4-120 letters, digits, "_", ".", ":" or "-" (it is the task\'s idempotency key)');
body.idempotency_key = `cli:${o.name}`;
}
++ if (o.effort) {
++ if (!['auto', 'low', 'medium', 'high', 'xhigh', 'max'].includes(o.effort)) throw new UsageError('--effort is auto, low, medium, high, xhigh or max');
++ body.effort = o.effort;
++ }
const answer = await call(c, 'POST', '/api/v1/tasks', body);
if (o.json) { out(JSON.stringify(answer, null, 2)); return EXIT.ok; }
const task = answer.task;
@@@ -309,6 +314,10 @@ async function cmdContinue(o, c) {
if (o.note) body.note = o.note;
const agent = agentOf(o.agent);
if (agent) body.agent = agent;
++ if (o.effort) {
++ if (!['auto', 'low', 'medium', 'high', 'xhigh', 'max'].includes(o.effort)) throw new UsageError('--effort is auto, low, medium, high, xhigh or max');
++ body.effort = o.effort;
++ }
const task = await call(c, 'POST', `/api/v1/tasks/${encodeURIComponent(id)}/continue`, body);
if (o.json) { out(JSON.stringify(task, null, 2)); return EXIT.ok; }
out(`carried on ${task.id} · ${stateOf(task)}`);
ddiff --git a/engine/adapters/base.js b/engine/adapters/base.js
iindex ecb5e06..ff57a4e 100644
---- a/engine/adapters/base.js
++++ b/engine/adapters/base.js
@@@ -76,12 +76,13 @@ const EFFORT_LEVELS = {
* harness, never handed over as it came.
*
* @param {string} id the harness
-- * @param {string|null} effort the wanted level
++ * @param {string|null} effort the wanted level ('auto' is the harness's own default: no setting)
* @returns {string|null} a level the harness accepts
*/
function effortFor(id, effort) {
++ if (!effort || effort === 'auto') return null;
const levels = EFFORT_LEVELS[id] || [];
-- if (!effort || !levels.length) return effort;
++ if (!levels.length) return effort;
if (levels.includes(effort)) return effort;
// The nearest level this harness accepts (a tie goes up), over the portal's own ordering.
const at = ORDER.indexOf(effort);
ddiff --git a/engine/adapters/codex.js b/engine/adapters/codex.js
iindex cbf8927..f42509a 100644
---- a/engine/adapters/codex.js
++++ b/engine/adapters/codex.js
@@@ -45,13 +45,13 @@ function build(ctx = {}) {
if (ctx.extraDir) roots.push(ctx.extraDir);
argv.push('resume', String(ctx.resume), '--json', '--skip-git-repo-check', '--all');
if (ctx.model) argv.push('-m', ctx.model);
-- argv.push('-c', `model_reasoning_effort=${effort || 'medium'}`);
++ if (effort) argv.push('-c', `model_reasoning_effort=${effort}`);
argv.push('-c', `sandbox_mode=${sandbox}`);
argv.push('-c', `sandbox_workspace_write.writable_roots=${JSON.stringify(roots)}`);
} else {
argv.push('--json', '--skip-git-repo-check', '-C', ctx.cwd, '-s', sandbox);
if (ctx.model) argv.push('-m', ctx.model);
-- argv.push('-c', `model_reasoning_effort=${effort || 'medium'}`);
++ if (effort) argv.push('-c', `model_reasoning_effort=${effort}`);
if (ctx.permission === 'edit' && ctx.extraDir) argv.push('--add-dir', ctx.extraDir);
}
for (const kv of (ctx.config || [])) argv.push('-c', kv);
ddiff --git a/engine/adapters/command-code.js b/engine/adapters/command-code.js
iindex 5d6e93a..cdd9c3c 100644
---- a/engine/adapters/command-code.js
++++ b/engine/adapters/command-code.js
@@@ -41,7 +41,7 @@ function build(ctx = {}) {
if (ctx.resume) argv.push('--resume', String(ctx.resume));
if (ctx.permission === 'full') argv.push('--yolo');
if (ctx.model) argv.push('-m', ctx.model);
-- argv.push('--effort', effort || 'high');
++ if (effort) argv.push('--effort', effort);
argv.push('--config', 'compact-mode=fast');
argv.push('--output-format', 'json');
argv.push('--max-turns', turnCap(ctx.maxTurns));
ddiff --git a/engine/effort.js b/engine/effort.js
iindex e134860..5c8cb60 100644
---- a/engine/effort.js
++++ b/engine/effort.js
@@@ -54,12 +54,13 @@ function harnessOf(id) {
* The nearest level the harness accepts for the wanted one (a tie goes up).
*
* @param {string} runner the harness
-- * @param {string|null} effort the wanted level
++ * @param {string|null} effort the wanted level ('auto' is the harness's own default: no setting)
* @returns {string|null} a level the harness accepts, or the wanted one when the harness has no effort setting
*/
function fitEffort(runner, effort) {
++ if (!effort || effort === 'auto') return null;
const list = EFFORTS[harnessOf(runner)] || [];
-- if (!effort || !list.length || list.includes(effort)) return effort;
++ if (!list.length || list.includes(effort)) return effort;
const at = ORDER.indexOf(effort);
if (at < 0) return effort;
for (let d = 0; d < ORDER.length; d++) for (const i of [at + d, at - d]) if (list.includes(ORDER[i])) return ORDER[i];
@@@ -76,7 +77,8 @@ function fitEffort(runner, effort) {
function stepUp(effort, list) {
const order = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
const i = order.indexOf(effort || 'medium');
-- for (let j = i + 1; j < order.length; j++) if (!list || list.includes(order[j])) return order[j];
++ const start = i < 0 ? order.indexOf('medium') : i;
++ for (let j = start + 1; j < order.length; j++) if (!list || list.includes(order[j])) return order[j];
return effort || null;
}
ddiff --git a/harness/adapters/cli.js b/harness/adapters/cli.js
iindex a1b152f..bd14e3a 100644
---- a/harness/adapters/cli.js
++++ b/harness/adapters/cli.js
@@@ -63,7 +63,7 @@ const claudeCode = {
const mode = o.permissionMode || { read: 'plan', edit: 'acceptEdits', full: 'bypassPermissions' }[t.permission] || 'plan';
const args = ['-p', t.prompt, '--output-format', 'stream-json', '--verbose', '--permission-mode', mode];
if (t.model) args.push('--model', t.model);
-- if (t.effort) args.push('--effort', effortFor('claude-code', t.effort));
++ { const effort = effortFor('claude-code', t.effort); if (effort) args.push('--effort', effort); }
if (t.resume) args.push('--resume', t.resume);
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
// MCP servers this session may use (the conductor's Codes tools): a config file, and the tools it may call
@@@ -97,7 +97,7 @@ const codex = {
? ['exec', 'resume', t.resume, '--json', '--skip-git-repo-check']
: ['exec', '--json', '--skip-git-repo-check', '-C', t.cwd, '-s', sandbox];
if (t.model) args.push('-m', t.model);
-- if (t.effort) args.push('-c', `model_reasoning_effort="${effortFor('codex', t.effort)}"`);
++ { const effort = effortFor('codex', t.effort); if (effort) args.push('-c', `model_reasoning_effort="${effort}"`); }
// An MCP server this session may use (the conductor's Codes tools, when Codex takes a conductor's turn): its
// command and arguments as config overrides, TOML values. Nothing secret rides here: the launcher holds the token.
const mcp = t.options && t.options.mcpServer;
@@@ -215,7 +215,7 @@ const commandCode = {
const args = [`--print=${t.prompt}`, '--output-format', 'json', '--skip-onboarding', '--no-auto-update', '-t'];
if (unattended(t)) args.push('--yolo');
if (t.model) args.push('-m', t.model);
-- if (t.effort) args.push('--effort', effortFor('command-code', t.effort));
++ { const effort = effortFor('command-code', t.effort); if (effort) args.push('--effort', effort); }
if (t.resume) args.push('--resume', t.resume);
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
const startedAt = Date.now();
@@@ -254,7 +254,7 @@ const aider = {
if (t.permission === 'edit' && !t.sandboxed) args.push('--no-suggest-shell-commands');
if (t.permission === 'read') args.push('--dry-run');
if (t.model) args.push('--model', t.model);
-- if (t.effort) args.push('--reasoning-effort', effortFor('aider', t.effort));
++ { const effort = effortFor('aider', t.effort); if (effort) args.push('--reasoning-effort', effort); }
const startedAt = Date.now();
const lines = [];
let started = false;
ddiff --git a/harness/job.js b/harness/job.js
iindex 12c273e..4778c12 100644
---- a/harness/job.js
++++ b/harness/job.js
@@@ -187,7 +187,7 @@ function parseArgs(raw, env = process.env) {
let a;
try { a = JSON.parse(String(raw || '').trim() || '{}'); } catch { throw new Error('the job input is not JSON'); }
if (!a || typeof a !== 'object' || Array.isArray(a)) throw new Error('the job input must be an object');
-- const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
++ const EFFORTS = ['auto', 'low', 'medium', 'high', 'xhigh', 'max'];
const effort = EFFORTS.includes(a.effort) ? a.effort : null;
// AL3: a job may ask the OpenVibe.AI agent for an advisor; the caps and the model choice belong to AI.
const advisor = a.advisor === true ? true
ddiff --git a/server/http/chat.js b/server/http/chat.js
iindex 57bcb38..54401e0 100644
---- a/server/http/chat.js
++++ b/server/http/chat.js
@@@ -756,7 +756,7 @@ function createChatRoutes(ctx) {
if (!who) return res.redirect(303, `/tasks/${encodeURIComponent(row.id)}?error=${encodeURIComponent('Choose who carries on from the list.')}`);
// The same agent with another model carries on; another agent takes over.
const agent = who.agent && who.agent !== row.agent ? who.agent : null;
-- const effort = ['low', 'medium', 'high', 'xhigh', 'max'].includes(b.effort) ? b.effort : null;
++ const effort = ['auto', 'low', 'medium', 'high', 'xhigh', 'max'].includes(b.effort) ? b.effort : null;
let ids = [];
try {
ids = await attachmentIds(req);
ddiff --git a/server/http/conductor-api.js b/server/http/conductor-api.js
iindex ae88bb8..ac8bebd 100644
---- a/server/http/conductor-api.js
++++ b/server/http/conductor-api.js
@@@ -225,9 +225,11 @@ function createConductorApi({ config, store, keys, harnesses, tasks, conductor,
const agent = wanted;
if (agent && !harnesses.list().some((h) => h.id === agent)) return problem(req, res, 422, 'codes.agent_unknown', `agent is one of GET /api/v1/harnesses, and not an Anthropic one`);
if (typeof b.repo !== 'string' || !/^https:\/\//.test(b.repo)) return problem(req, res, 422, 'codes.task_invalid', 'a sub-task needs the repository it works in (https://…)');
++ const effort = b.effort == null || b.effort === '' ? null : String(b.effort);
++ if (effort != null && !['auto', 'low', 'medium', 'high', 'xhigh', 'max'].includes(effort)) return problem(req, res, 422, 'codes.effort_invalid', 'effort is auto, low, medium, high, xhigh or max');
let args;
try {
-- args = parseArgs(JSON.stringify({ repo: b.repo, ref: b.ref || null, prompt, ...(agent ? { agent } : {}), ...(model ? { model } : {}), permission: 'edit' }));
++ args = parseArgs(JSON.stringify({ repo: b.repo, ref: b.ref || null, prompt, ...(agent ? { agent } : {}), ...(model ? { model } : {}), ...(effort ? { effort } : {}), permission: 'edit' }));
} catch (e) {
return problem(req, res, 422, 'codes.task_invalid', e.message);
}
ddiff --git a/server/http/tasks-api.js b/server/http/tasks-api.js
iindex db803ff..8203892 100644
---- a/server/http/tasks-api.js
++++ b/server/http/tasks-api.js
@@@ -188,6 +188,7 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
delete bodyForValidate.actor_task_id;
delete bodyForValidate.actor_step_id;
delete bodyForValidate.goal;
++ delete bodyForValidate.effort;
const check = validate('codes.task-start-request@1', bodyForValidate);
if (!check.valid) return problem(req, res, 422, 'codes.task_invalid', check.errors.map((e) => `${e.instancePath || 'body'} ${e.message}`).join('; ').slice(0, 500));
let goal = null;
@@@ -199,6 +200,11 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
goal = { goal: g.goal.trim(), check: { kind: g.check.kind, value: g.check.value.trim() }, budget: limits, no_progress_limit: g.no_progress_limit || 3, proposal: null, evidence: null, verdict: null };
}
if (b.agent && !harnesses.list().some((a) => a.id === b.agent)) return problem(req, res, 422, 'codes.agent_unknown', 'agent is one of GET /api/v1/harnesses, or absent for the router to pick');
++ // The effort the task asks for (ai.effort@1 plus xhigh; auto is the harness default, resolved by AI's own
++ // role and task defaults on the AI path). The contract does not carry it yet, so like the goal above it is
++ // stripped before validation; when the contract is amended, pass it through instead.
++ const effort = b.effort == null || b.effort === '' ? null : String(b.effort);
++ if (effort != null && !['auto', 'low', 'medium', 'high', 'xhigh', 'max'].includes(effort)) return problem(req, res, 422, 'codes.effort_invalid', 'effort is auto, low, medium, high, xhigh or max');
const p = req.principal;
const key = b.idempotency_key || null;
if (key) {
@@@ -209,6 +215,7 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
try {
args = parseArgs(JSON.stringify({
repo: b.repo, ref: b.ref || null, prompt: b.prompt, agent: b.agent || null, permission: b.permission || 'edit', ...(goal ? { goal } : {}),
++ ...(effort ? { effort } : {}),
...(b.push ? { push: { pull_request: b.pull_request === true } } : {}),
}));
} catch (e) {
@@@ -349,7 +356,7 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
const note = typeof b.note === 'string' ? b.note.slice(0, 4000) : null;
const agent = typeof b.agent === 'string' && b.agent ? b.agent : null;
if (agent && !harnesses.list().some((a) => a.id === agent)) return problem(req, res, 422, 'codes.agent_unknown', 'agent is one of GET /api/v1/harnesses');
-- const EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max'];
++ const EFFORTS = ['auto', 'low', 'medium', 'high', 'xhigh', 'max'];
const model = typeof b.model === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:\/#-]{0,99}$/.test(b.model) ? b.model : null;
const effort = EFFORTS.includes(b.effort) ? b.effort : null;
const out = await taskStore.continueTask(row, { note: note || (typeof b.text === 'string' ? b.text.slice(0, 4000) : null), agent, model, effort });
ddiff --git a/test/conductor.test.js b/test/conductor.test.js
iindex b338794..0deaba4 100644
---- a/test/conductor.test.js
++++ b/test/conductor.test.js
@@@ -192,6 +192,18 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2N1';
assert.strictEqual(subRow.via, 'conductor');
});
++ await check('a sub-task takes the effort it is given, and a level that is none is 422', async () => {
++ const asked = await post(`/api/v1/conductor/${taskId}/tasks`, { prompt: 'think hard', repo: 'https://github.com/octocat/Hello-World', effort: 'high' });
++ assert.strictEqual(asked.status, 201, asked.text);
++ assert.strictEqual(submits().at(-1)[1].args.effort, 'high');
++ const auto = await post(`/api/v1/conductor/${taskId}/tasks`, { prompt: 'think as usual', repo: 'https://github.com/octocat/Hello-World', effort: 'auto' });
++ assert.strictEqual(auto.status, 201, auto.text);
++ assert.strictEqual(submits().at(-1)[1].args.effort, 'auto');
++ const bad = await post(`/api/v1/conductor/${taskId}/tasks`, { prompt: 'think', repo: 'https://github.com/octocat/Hello-World', effort: 'ludicrous' });
++ assert.strictEqual(bad.status, 422, bad.text);
++ assert.strictEqual(bad.json().code, 'codes.effort_invalid');
++ });
++
await check('the start form: a model list the login offers, and the folders the machine has worked in', async () => {
const res = await get('/conductor');
assert.strictEqual(res.status, 200);
ddiff --git a/test/engine-adapters.test.js b/test/engine-adapters.test.js
iindex 25c5e08..e7a98ae 100644
---- a/test/engine-adapters.test.js
++++ b/test/engine-adapters.test.js
@@@ -216,10 +216,21 @@ function dry(adapter, ctx = {}) {
assert.equal(effortFor('claude-code', 'medium'), 'medium');
assert.equal(effortFor('opencode', 'medium'), 'medium', 'OpenCode takes no effort setting');
assert.equal(effortFor('codex', null), null);
++ assert.equal(effortFor('codex', 'auto'), null, 'auto is the harness default: no setting');
// And the adapter's own argv carries the fitted level.
assert.ok(dry(commandCode, { effort: 'medium' }).argv.includes('high'));
});
++ await check('no effort, no effort setting: nothing is hardcoded', () => {
++ const fresh = dry(codex, { permission: 'edit', cwd: '/tmp/work' });
++ assert.ok(!fresh.argv.some((a) => /model_reasoning_effort/.test(a)), `Codex: ${fresh.argv.join(' ')}`);
++ const resumed = dry(codex, { resume: 'thread-7', permission: 'edit', cwd: '/tmp/work' });
++ assert.ok(!resumed.argv.some((a) => /model_reasoning_effort/.test(a)), `Codex resume: ${resumed.argv.join(' ')}`);
++ const cmd = dry(commandCode, { permission: 'edit', maxTurns: 150 });
++ assert.ok(!cmd.argv.includes('--effort'), `Command Code: ${cmd.argv.join(' ')}`);
++ assert.ok(!dry(commandCode, { effort: 'auto' }).argv.includes('--effort'), 'auto takes no flag either');
++ });
++
await check('each feed reads its harness into the one transcript shape', () => {
// Codex
const cx = [];
ddiff --git a/test/engine-effort.test.js b/test/engine-effort.test.js
iindex 572b45c..6749c34 100644
---- a/test/engine-effort.test.js
++++ b/test/engine-effort.test.js
@@@ -55,6 +55,14 @@ const ALL = ['off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max',
assert.equal(stepUp('medium', EFFORTS.cc), 'high');
assert.equal(stepUp('max', EFFORTS.cc), 'max');
assert.equal(stepUp('medium', EFFORTS.cx), 'high');
++ assert.equal(stepUp('auto', EFFORTS.cx), 'high', 'an unknown level retries from medium, not from the bottom');
++ assert.equal(stepUp(null, EFFORTS.cx), 'high');
++ });
++
++ await check('auto is no setting: the harness default applies', () => {
++ assert.equal(fitEffort('cc', 'auto'), null);
++ assert.equal(fitEffort('cx', 'auto'), null);
++ assert.equal(fitEffort('oc', 'auto'), null);
});
await check('a blocked model is replaced by one the harness still lists', () => {
ddiff --git a/test/harness-effort.test.js b/test/harness-effort.test.js
iindex 28c6182..963e002 100644
---- a/test/harness-effort.test.js
++++ b/test/harness-effort.test.js
@@@ -53,8 +53,19 @@ fs.writeFileSync(process.env.FAKE_RECORD, JSON.stringify({ argv: process.argv.sl
assert.ok(!(await argvOf(cli.opencode, 'max')).some((a) => /effort/.test(a)), 'OpenCode');
});
++ await check('auto is each CLI\'s own default: no flag, never the word auto', async () => {
++ assert.ok(!(await argvOf(cli.claudeCode, 'auto')).includes('--effort'), 'Claude Code');
++ const codex = await argvOf(cli.codex, 'auto');
++ assert.ok(!codex.some((a) => /reasoning_effort/.test(a)) && !codex.includes('auto'), `Codex: ${codex.join(' ')}`);
++ const cmd = await argvOf(cli.commandCode, 'auto');
++ assert.ok(!cmd.includes('--effort') && !cmd.includes('auto'), `Command Code: ${cmd.join(' ')}`);
++ const aider = await argvOf(cli.aider, 'auto');
++ assert.ok(!aider.includes('--reasoning-effort') && !aider.includes('auto'), `Aider: ${aider.join(' ')}`);
++ });
++
await check('the job input keeps an effort on the ladder and drops anything else', () => {
assert.strictEqual(parseArgs(JSON.stringify({ repo: 'https://github.com/a/b', prompt: 'x', effort: 'max' })).effort, 'max');
++ assert.strictEqual(parseArgs(JSON.stringify({ repo: 'https://github.com/a/b', prompt: 'x', effort: 'auto' })).effort, 'auto');
assert.strictEqual(parseArgs(JSON.stringify({ repo: 'https://github.com/a/b', prompt: 'x', effort: 'ludicrous' })).effort, undefined);
assert.strictEqual(parseArgs(JSON.stringify({ kind: 'conductor', folder: '/home/x', prompt: 'x', effort: 'low' })).effort, 'low');
});
ddiff --git a/test/mcp.test.js b/test/mcp.test.js
iindex ea29417..fa66782 100644
---- a/test/mcp.test.js
++++ b/test/mcp.test.js
@@@ -120,6 +120,17 @@ async function fakeGitHub() {
subTask = out.id;
});
++ await check('codes_task_start: the effort it asks for reaches the sub-task', async () => {
++ const before = calls.length;
++ const out = await mcp.call('codes_task_start', { prompt: 'Fix the parser and add a test.', repo: 'octocat/Hello-World', agent: 'codex', effort: 'low' });
++ assert.ok(out.id, JSON.stringify(out));
++ assert.strictEqual(calls.length, before + 1);
++ assert.strictEqual(calls.at(-1)[1].args.effort, 'low');
++ assert.ok(!('note' in out), 'effort is a field now: no note about it');
++ const machine = await mcp.call('codes_task_start', { prompt: 'Fix the parser and add a test.', repo: 'octocat/Hello-World', agent: 'codex', machine: 'ada-laptop' });
++ assert.match(machine.note || '', /machine is not a field/);
++ });
++
await check('codes_task_start: the rule, refused here and not sent', async () => {
const anthropic = await mcp.call('codes_task_start', { prompt: 'x', repo, agent: 'claude-code' });
assert.match(anthropic.error, /Anthropic/);
ddiff --git a/test/task-cli.test.js b/test/task-cli.test.js
iindex adb81c9..2b98c60 100644
---- a/test/task-cli.test.js
++++ b/test/task-cli.test.js
@@@ -102,6 +102,20 @@ function taskDoc(over = {}) {
assert.match(s.body.prompt, /tier: hard\s*$/);
});
++ await check('start and continue carry --effort; a level that is none is exit 2', async () => {
++ const brief = path.join(dir, 'brief.md');
++ const r = await run(['start', '--repo', 'acme/thing', '--file', brief, '--name', 'c6-effort', '--effort', 'low']);
++ assert.strictEqual(r.code, 0, r.err + r.out);
++ assert.strictEqual(last().body.effort, 'low');
++ const c = await run(['continue', ID, '--effort', 'max']);
++ assert.strictEqual(c.code, 0, c.err + c.out);
++ assert.strictEqual(last().body.effort, 'max');
++ assert.strictEqual(last().path, `/api/v1/tasks/${ID}/continue`);
++ const bad = await run(['start', '--repo', 'acme/thing', '--file', brief, '--effort', 'ludicrous']);
++ assert.strictEqual(bad.code, 2);
++ assert.match(bad.err + bad.out, /--effort is auto, low, medium, high, xhigh or max/);
++ });
++
await check('start twice with one --name answers the first task', async () => {
const brief = path.join(dir, 'brief.md');
const r = await run(['start', '--repo', 'https://github.com/acme/thing', '--file', brief, '--name', 'c6-run']);
ddiff --git a/test/tasks-api.test.js b/test/tasks-api.test.js
iindex ab7592a..c7738ac 100644
---- a/test/tasks-api.test.js
++++ b/test/tasks-api.test.js
@@@ -80,6 +80,21 @@ const { boot, check, done } = require('./helpers/boot');
assert.strictEqual(calls.filter((c) => c[0] === 'submit').length, 1, 'one job');
});
++ await check('a task starts with the effort it asks for; a level that is none is 422', async () => {
++ const r = await call('POST', '/tasks', { body: { ...ask, idempotency_key: 'act_task_1:effort', effort: 'high' } });
++ assert.strictEqual(r.status, 201, JSON.stringify(r.json));
++ const submit = calls.at(-1)[1];
++ assert.strictEqual(submit.args.effort, 'high');
++ const attempt = await db.one('SELECT effort FROM codes_attempts WHERE task_id = $1 ORDER BY n', [r.json.task.id]);
++ assert.strictEqual(attempt.effort, 'high');
++ const auto = await call('POST', '/tasks', { body: { ...ask, idempotency_key: 'act_task_1:auto', effort: 'auto' } });
++ assert.strictEqual(auto.status, 201, JSON.stringify(auto.json));
++ assert.strictEqual(calls.at(-1)[1].args.effort, 'auto');
++ const bad = await call('POST', '/tasks', { body: { ...ask, idempotency_key: 'act_task_1:bad', effort: 'ludicrous' } });
++ assert.strictEqual(bad.status, 422);
++ assert.strictEqual(bad.json.code, 'codes.effort_invalid');
++ });
++
await check('reading follows Run; the machine’s result is passed on only as codes.task@1 allows', async () => {
state = 'succeeded';
result = {