openvibers/openvibe.codes Commits
Effort on every run path; no hardcoded level, auto means the default (R1 item 1) (#378)
Changes
@@@ -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' } : {}),});}@@@ -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 advicemid-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)@@@ -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); fullopenvibe-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)}`);@@@ -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);@@@ -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);@@@ -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));@@@ -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;}@@@ -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;@@@ -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@@@ -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);@@@ -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);}@@@ -188,6 +188,7 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, taskdelete 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, taskgoal = { 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, tasktry {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, taskconst 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 });@@@ -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);@@@ -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', () => {// Codexconst cx = [];@@@ -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', () => {@@@ -53,8 +53,19 @@ fs.writeFileSync(process.env.FAKE_RECORD, JSON.stringify({ argv: process.argv.slassert.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');});@@@ -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/);@@@ -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']);@@@ -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 = {
@@ -70,6 +70,7 @@ const TOOLS = [
@@ -70,6 +70,7 @@ const TOOLS = [
agent: { type: 'string', description: 'codex, opencode, command-code, deepseek, aider: never claude-code or claude-other' },
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)' },
model: { type: 'string', description: 'The model for that agent (never an Anthropic one)' },
ref: { type: 'string', description: 'A branch or tag to start from' },
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'],
required: ['prompt', 'repo'],
additionalProperties: true,
additionalProperties: true,
@@ -135,13 +136,15 @@ async function startTask(args) {
@@ -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 (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');
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 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`, {
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) } : {}),
prompt, repo, ...(agent ? { agent } : {}), ...(model ? { model } : {}), ...(args.ref ? { ref: String(args.ref).slice(0, 100) } : {}),
+ ...(effort ? { effort } : {}),
});
});
const t = answer.task || {};
const t = answer.task || {};
return text({
return text({
id: t.id, state: t.state, agent: t.agent, model: model || null, repo: t.repo, url: t.url,
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' } : {}),
});
});
}
}
@@ -27,8 +27,9 @@ const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your term
@@ -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,
--agent <id> skip routing: claude-code, claude-other, codex, opencode, command-code,
aider, minimax-code, deepseek, openai-compatible
aider, minimax-code, deepseek, openai-compatible
--model <name> the first agent's model
--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
--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)
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)
--advisor-model <provider/model> the advisor model (default: AI's reviewer role, then its strongest)
@@ -6,10 +6,10 @@
@@ -6,10 +6,10 @@
* this machine: a task runs on one of your machines through OpenVibe.Run, and this client only talks to
* 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).
* 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 list [--state running|needs|done]
* openvibe-codes task show <id> [--follow]
* 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>
* openvibe-codes task stop <id>
*
*
* Who you are: a personal Network access token, from OPENVIBE_TOKEN or a file (~/.config/openvibe/token, mode 600),
* 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
@@ -44,10 +44,11 @@ const USAGE = `openvibe-codes task — coding tasks on OpenVibe.Codes, as you
--file <path|-> the brief ("-" reads it from stdin)
--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
--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)
--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
--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 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 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>
openvibe-codes task stop <id>
--json prints the API's answer as JSON instead of a readable log.
--json prints the API's answer as JSON instead of a readable log.
@@ -70,7 +71,7 @@ function parse(argv) {
@@ -70,7 +71,7 @@ function parse(argv) {
else if (a === '--help' || a === '-h') o.help = true;
else if (a === '--help' || a === '-h') o.help = true;
else if (a.startsWith('--')) {
else if (a.startsWith('--')) {
const k = a.slice(2);
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];
const v = argv[++i];
if (v === undefined || v.startsWith('--')) throw new UsageError(`--${k} needs a value`);
if (v === undefined || v.startsWith('--')) throw new UsageError(`--${k} needs a value`);
o[k] = v;
o[k] = v;
@@ -261,6 +262,10 @@ async function cmdStart(o, c) {
@@ -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)');
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}`;
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);
const answer = await call(c, 'POST', '/api/v1/tasks', body);
if (o.json) { out(JSON.stringify(answer, null, 2)); return EXIT.ok; }
if (o.json) { out(JSON.stringify(answer, null, 2)); return EXIT.ok; }
const task = answer.task;
const task = answer.task;
@@ -309,6 +314,10 @@ async function cmdContinue(o, c) {
@@ -309,6 +314,10 @@ async function cmdContinue(o, c) {
if (o.note) body.note = o.note;
if (o.note) body.note = o.note;
const agent = agentOf(o.agent);
const agent = agentOf(o.agent);
if (agent) body.agent = 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);
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; }
if (o.json) { out(JSON.stringify(task, null, 2)); return EXIT.ok; }
out(`carried on ${task.id} · ${stateOf(task)}`);
out(`carried on ${task.id} · ${stateOf(task)}`);
@@ -76,12 +76,13 @@ const EFFORT_LEVELS = {
@@ -76,12 +76,13 @@ const EFFORT_LEVELS = {
* harness, never handed over as it came.
* harness, never handed over as it came.
*
*
* @param {string} id the harness
* @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
* @returns {string|null} a level the harness accepts
*/
*/
function effortFor(id, effort) {
function effortFor(id, effort) {
+ if (!effort || effort === 'auto') return null;
const levels = EFFORT_LEVELS[id] || [];
const levels = EFFORT_LEVELS[id] || [];
- if (!effort || !levels.length) return effort;
+ if (!levels.length) return effort;
if (levels.includes(effort)) return effort;
if (levels.includes(effort)) return effort;
// The nearest level this harness accepts (a tie goes up), over the portal's own ordering.
// The nearest level this harness accepts (a tie goes up), over the portal's own ordering.
const at = ORDER.indexOf(effort);
const at = ORDER.indexOf(effort);
@@ -45,13 +45,13 @@ function build(ctx = {}) {
@@ -45,13 +45,13 @@ function build(ctx = {}) {
if (ctx.extraDir) roots.push(ctx.extraDir);
if (ctx.extraDir) roots.push(ctx.extraDir);
argv.push('resume', String(ctx.resume), '--json', '--skip-git-repo-check', '--all');
argv.push('resume', String(ctx.resume), '--json', '--skip-git-repo-check', '--all');
if (ctx.model) argv.push('-m', ctx.model);
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_mode=${sandbox}`);
argv.push('-c', `sandbox_workspace_write.writable_roots=${JSON.stringify(roots)}`);
argv.push('-c', `sandbox_workspace_write.writable_roots=${JSON.stringify(roots)}`);
} else {
} else {
argv.push('--json', '--skip-git-repo-check', '-C', ctx.cwd, '-s', sandbox);
argv.push('--json', '--skip-git-repo-check', '-C', ctx.cwd, '-s', sandbox);
if (ctx.model) argv.push('-m', ctx.model);
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);
if (ctx.permission === 'edit' && ctx.extraDir) argv.push('--add-dir', ctx.extraDir);
}
}
for (const kv of (ctx.config || [])) argv.push('-c', kv);
for (const kv of (ctx.config || [])) argv.push('-c', kv);
@@ -41,7 +41,7 @@ function build(ctx = {}) {
@@ -41,7 +41,7 @@ function build(ctx = {}) {
if (ctx.resume) argv.push('--resume', String(ctx.resume));
if (ctx.resume) argv.push('--resume', String(ctx.resume));
if (ctx.permission === 'full') argv.push('--yolo');
if (ctx.permission === 'full') argv.push('--yolo');
if (ctx.model) argv.push('-m', ctx.model);
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('--config', 'compact-mode=fast');
argv.push('--output-format', 'json');
argv.push('--output-format', 'json');
argv.push('--max-turns', turnCap(ctx.maxTurns));
argv.push('--max-turns', turnCap(ctx.maxTurns));
@@ -54,12 +54,13 @@ function harnessOf(id) {
@@ -54,12 +54,13 @@ function harnessOf(id) {
* The nearest level the harness accepts for the wanted one (a tie goes up).
* The nearest level the harness accepts for the wanted one (a tie goes up).
*
*
* @param {string} runner the harness
* @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
* @returns {string|null} a level the harness accepts, or the wanted one when the harness has no effort setting
*/
*/
function fitEffort(runner, effort) {
function fitEffort(runner, effort) {
+ if (!effort || effort === 'auto') return null;
const list = EFFORTS[harnessOf(runner)] || [];
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);
const at = ORDER.indexOf(effort);
if (at < 0) return 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];
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) {
@@ -76,7 +77,8 @@ function fitEffort(runner, effort) {
function stepUp(effort, list) {
function stepUp(effort, list) {
const order = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
const order = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
const i = order.indexOf(effort || 'medium');
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;
return effort || null;
}
}
@@ -63,7 +63,7 @@ const claudeCode = {
@@ -63,7 +63,7 @@ const claudeCode = {
const mode = o.permissionMode || { read: 'plan', edit: 'acceptEdits', full: 'bypassPermissions' }[t.permission] || 'plan';
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];
const args = ['-p', t.prompt, '--output-format', 'stream-json', '--verbose', '--permission-mode', mode];
if (t.model) args.push('--model', t.model);
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.resume) args.push('--resume', t.resume);
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
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
// 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 = {
@@ -97,7 +97,7 @@ const codex = {
? ['exec', 'resume', t.resume, '--json', '--skip-git-repo-check']
? ['exec', 'resume', t.resume, '--json', '--skip-git-repo-check']
: ['exec', '--json', '--skip-git-repo-check', '-C', t.cwd, '-s', sandbox];
: ['exec', '--json', '--skip-git-repo-check', '-C', t.cwd, '-s', sandbox];
if (t.model) args.push('-m', t.model);
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
// 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.
// command and arguments as config overrides, TOML values. Nothing secret rides here: the launcher holds the token.
const mcp = t.options && t.options.mcpServer;
const mcp = t.options && t.options.mcpServer;
@@ -215,7 +215,7 @@ const commandCode = {
@@ -215,7 +215,7 @@ const commandCode = {
const args = [`--print=${t.prompt}`, '--output-format', 'json', '--skip-onboarding', '--no-auto-update', '-t'];
const args = [`--print=${t.prompt}`, '--output-format', 'json', '--skip-onboarding', '--no-auto-update', '-t'];
if (unattended(t)) args.push('--yolo');
if (unattended(t)) args.push('--yolo');
if (t.model) args.push('-m', t.model);
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.resume) args.push('--resume', t.resume);
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
if (t.maxTurns) args.push('--max-turns', String(t.maxTurns));
const startedAt = Date.now();
const startedAt = Date.now();
@@ -254,7 +254,7 @@ const aider = {
@@ -254,7 +254,7 @@ const aider = {
if (t.permission === 'edit' && !t.sandboxed) args.push('--no-suggest-shell-commands');
if (t.permission === 'edit' && !t.sandboxed) args.push('--no-suggest-shell-commands');
if (t.permission === 'read') args.push('--dry-run');
if (t.permission === 'read') args.push('--dry-run');
if (t.model) args.push('--model', t.model);
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 startedAt = Date.now();
const lines = [];
const lines = [];
let started = false;
let started = false;
@@ -187,7 +187,7 @@ function parseArgs(raw, env = process.env) {
@@ -187,7 +187,7 @@ function parseArgs(raw, env = process.env) {
let a;
let a;
try { a = JSON.parse(String(raw || '').trim() || '{}'); } catch { throw new Error('the job input is not JSON'); }
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');
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;
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.
// 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
const advisor = a.advisor === true ? true
@@ -756,7 +756,7 @@ function createChatRoutes(ctx) {
@@ -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.')}`);
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.
// The same agent with another model carries on; another agent takes over.
const agent = who.agent && who.agent !== row.agent ? who.agent : null;
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 = [];
let ids = [];
try {
try {
ids = await attachmentIds(req);
ids = await attachmentIds(req);
@@ -225,9 +225,11 @@ function createConductorApi({ config, store, keys, harnesses, tasks, conductor,
@@ -225,9 +225,11 @@ function createConductorApi({ config, store, keys, harnesses, tasks, conductor,
const agent = wanted;
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 (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://…)');
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;
let args;
try {
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) {
} catch (e) {
return problem(req, res, 422, 'codes.task_invalid', e.message);
return problem(req, res, 422, 'codes.task_invalid', e.message);
}
}
@@ -188,6 +188,7 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
@@ -188,6 +188,7 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
delete bodyForValidate.actor_task_id;
delete bodyForValidate.actor_task_id;
delete bodyForValidate.actor_step_id;
delete bodyForValidate.actor_step_id;
delete bodyForValidate.goal;
delete bodyForValidate.goal;
+ delete bodyForValidate.effort;
const check = validate('codes.task-start-request@1', bodyForValidate);
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));
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;
let goal = null;
@@ -199,6 +200,11 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
@@ -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 };
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');
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 p = req.principal;
const key = b.idempotency_key || null;
const key = b.idempotency_key || null;
if (key) {
if (key) {
@@ -209,6 +215,7 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
@@ -209,6 +215,7 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
try {
try {
args = parseArgs(JSON.stringify({
args = parseArgs(JSON.stringify({
repo: b.repo, ref: b.ref || null, prompt: b.prompt, agent: b.agent || null, permission: b.permission || 'edit', ...(goal ? { goal } : {}),
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 } } : {}),
...(b.push ? { push: { pull_request: b.pull_request === true } } : {}),
}));
}));
} catch (e) {
} catch (e) {
@@ -349,7 +356,7 @@ function createTasksApi({ config, store, keys, run, harnesses, actorLimits, task
@@ -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 note = typeof b.note === 'string' ? b.note.slice(0, 4000) : null;
const agent = typeof b.agent === 'string' && b.agent ? b.agent : 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');
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 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 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 });
const out = await taskStore.continueTask(row, { note: note || (typeof b.text === 'string' ? b.text.slice(0, 4000) : null), agent, model, effort });
@@ -192,6 +192,18 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2N1';
@@ -192,6 +192,18 @@ const DEVICE = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2N1';
assert.strictEqual(subRow.via, 'conductor');
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 () => {
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');
const res = await get('/conductor');
assert.strictEqual(res.status, 200);
assert.strictEqual(res.status, 200);
@@ -216,10 +216,21 @@ function dry(adapter, ctx = {}) {
@@ -216,10 +216,21 @@ function dry(adapter, ctx = {}) {
assert.equal(effortFor('claude-code', 'medium'), 'medium');
assert.equal(effortFor('claude-code', 'medium'), 'medium');
assert.equal(effortFor('opencode', 'medium'), 'medium', 'OpenCode takes no effort setting');
assert.equal(effortFor('opencode', 'medium'), 'medium', 'OpenCode takes no effort setting');
assert.equal(effortFor('codex', null), null);
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.
// And the adapter's own argv carries the fitted level.
assert.ok(dry(commandCode, { effort: 'medium' }).argv.includes('high'));
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', () => {
await check('each feed reads its harness into the one transcript shape', () => {
// Codex
// Codex
const cx = [];
const cx = [];
@@ -55,6 +55,14 @@ const ALL = ['off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max',
@@ -55,6 +55,14 @@ const ALL = ['off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max',
assert.equal(stepUp('medium', EFFORTS.cc), 'high');
assert.equal(stepUp('medium', EFFORTS.cc), 'high');
assert.equal(stepUp('max', EFFORTS.cc), 'max');
assert.equal(stepUp('max', EFFORTS.cc), 'max');
assert.equal(stepUp('medium', EFFORTS.cx), 'high');
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', () => {
await check('a blocked model is replaced by one the harness still lists', () => {
@@ -53,8 +53,19 @@ fs.writeFileSync(process.env.FAKE_RECORD, JSON.stringify({ argv: process.argv.sl
@@ -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');
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', () => {
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: '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({ 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');
assert.strictEqual(parseArgs(JSON.stringify({ kind: 'conductor', folder: '/home/x', prompt: 'x', effort: 'low' })).effort, 'low');
});
});
@@ -120,6 +120,17 @@ async function fakeGitHub() {
@@ -120,6 +120,17 @@ async function fakeGitHub() {
subTask = out.id;
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 () => {
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' });
const anthropic = await mcp.call('codes_task_start', { prompt: 'x', repo, agent: 'claude-code' });
assert.match(anthropic.error, /Anthropic/);
assert.match(anthropic.error, /Anthropic/);
@@ -102,6 +102,20 @@ function taskDoc(over = {}) {
@@ -102,6 +102,20 @@ function taskDoc(over = {}) {
assert.match(s.body.prompt, /tier: hard\s*$/);
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 () => {
await check('start twice with one --name answers the first task', async () => {
const brief = path.join(dir, 'brief.md');
const brief = path.join(dir, 'brief.md');
const r = await run(['start', '--repo', 'https://github.com/acme/thing', '--file', brief, '--name', 'c6-run']);
const r = await run(['start', '--repo', 'https://github.com/acme/thing', '--file', brief, '--name', 'c6-run']);
@@ -80,6 +80,21 @@ const { boot, check, done } = require('./helpers/boot');
@@ -80,6 +80,21 @@ const { boot, check, done } = require('./helpers/boot');
assert.strictEqual(calls.filter((c) => c[0] === 'submit').length, 1, 'one job');
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 () => {
await check('reading follows Run; the machine’s result is passed on only as codes.task@1 allows', async () => {
state = 'succeeded';
state = 'succeeded';
result = {
result = {