openvibers/openvibe.codes Commits
The CLI an agent can drive: --for-agent and a shipped guide (#368)
Changes
nnew file mode 100644@@@ -0,0 +1,138 @@++'use strict';++++/**++ * `--for-agent`: the machine face of `openvibe-codes`.++ *++ * A supervising agent asks the CLI with `--for-agent` and reads one compact JSON object per line: no ANSI, no pretty++ * printing, no full event dump. The fields are fixed so a caller can branch on them without parsing words:++ *++ * run the run id (the same one `show` and `resume` take)++ * seq the line's number in this run, from 1++ * event init | text | tool | tool_error | progress | handoff | warning | stderr | result | run | attempt | error++ * agent the harness that produced the line (init, result and the run summaries)++ * session the harness's session id, when it has one (the key `resume` needs)++ * ref what a tool call points at: a file, a pattern, a URL or the first line of a command++ * ok on a result: did the agent finish without an error++ * error on a failure: { code, retry, suggestion }; retry is true only when repeating the same command is safe++ *++ * Text is bounded (MAX_TEXT) because a supervising agent pays for every token of it: the human log stays the full one.++ * The guide that explains these fields ships with the binary and matches its version: `openvibe-codes skills get`.++ */++++const MAX_TEXT = 400;++const MAX_REF = 200;++++// The closed set of codes. A caller branches on the code, never on the message.++const ERRORS = {++ no_prompt: { retry: false, suggestion: 'pass the task as an argument, or "-" and write it on stdin' },++ bad_agent: { retry: false, suggestion: 'run `openvibe-codes agents --for-agent` and pick an id it lists as available' },++ unavailable: { retry: false, suggestion: 'run `openvibe-codes agents --for-agent`; the harness is not installed or signed in here' },++ agent_failed: { retry: false, suggestion: 'read the result, then continue the same session with `openvibe-codes resume <run> "<follow-up>"`' },++ no_run: { retry: false, suggestion: 'run `openvibe-codes runs --for-agent` and use an id from it' },++ usage: { retry: false, suggestion: 'run `openvibe-codes skills get` for the command and output reference' },++ internal: { retry: false, suggestion: 'run the same command without --for-agent to see the full error' },++};++++const clip = (s, n) => { const t = String(s == null ? '' : s); return t.length > n ? `${t.slice(0, n - 1)}…` : t; };++++/** One JSON line. `undefined` fields are left out, so the envelope stays small. */++function line(doc) {++ const out = {};++ for (const [k, v] of Object.entries(doc)) if (v !== undefined && v !== null && v !== '') out[k] = v;++ return JSON.stringify(out);++}++++/** A failure the caller can act on: a code, whether repeating the command is safe, and what to do. */++function errorLine(code, message, extra = {}) {++ const known = ERRORS[code] || ERRORS.internal;++ return line({++ event: 'error', code, retry: extra.retry != null ? !!extra.retry : known.retry,++ suggestion: extra.suggestion || known.suggestion, message: clip(message, MAX_TEXT),++ ...(extra.run ? { run: extra.run } : {}), ...(extra.seq ? { seq: extra.seq } : {}),++ });++}++++/** What a tool call points at, in the harness's own vocabulary (Claude Code's names are the shared ones). */++function refOf(input) {++ const i = input && typeof input === 'object' ? input : {};++ for (const key of ['file_path', 'path', 'notebook_path', 'pattern', 'query', 'url', 'command', 'description']) {++ const v = i[key];++ if (typeof v === 'string' && v.trim()) return clip(v.split('\n')[0].trim(), MAX_REF);++ }++ return undefined;++}++++/** The lines one harness event becomes. An assistant message may hold text and tool calls: one line each. */++function eventLines(runId, seq, e) {++ if (!e || typeof e !== 'object') return [];++ const base = { run: runId, ...(seq == null ? {} : { seq }) };++ const lines = [];++ if (e.type === 'system') {++ if (e.subtype === 'init') lines.push(line({ ...base, event: 'init', agent: e.agent, model: e.model, session: e.session_id }));++ else if (e.subtype === 'handoff') lines.push(line({ ...base, event: 'handoff', from: e.from, reason: clip(e.reason, MAX_TEXT) }));++ else if (e.subtype === 'warning' || e.subtype === 'error') lines.push(line({ ...base, event: 'warning', level: e.subtype, message: clip(e.text, MAX_TEXT) }));++ else if (e.subtype === 'stderr') lines.push(line({ ...base, event: 'stderr', message: clip(e.text, MAX_TEXT) }));++ else if (e.subtype === 'tool_progress') lines.push(line({ ...base, event: 'progress', tool: e.tool_use_id, text: clip(e.text, MAX_TEXT) }));++ return lines;++ }++ if (e.type === 'assistant') {++ for (const c of (e.message && e.message.content) || []) {++ if (c.type === 'text' && String(c.text || '').trim()) lines.push(line({ ...base, event: 'text', agent: e._agent, text: clip(c.text, MAX_TEXT) }));++ else if (c.type === 'tool_use') lines.push(line({ ...base, event: 'tool', agent: e._agent, tool: c.name, ref: refOf(c.input) }));++ }++ return lines;++ }++ if (e.type === 'user') {++ for (const c of (e.message && e.message.content) || []) {++ if (c.type === 'tool_result' && c.is_error) lines.push(line({ ...base, event: 'tool_error', text: clip(c.content, MAX_TEXT) }));++ }++ return lines;++ }++ if (e.type === 'result') {++ const failed = !!e.is_error;++ // "X is not available here" is the harness refusing before an agent ran, not an agent's own failure.++ const code = failed && /is not available here/.test(String(e.result || '')) ? 'unavailable' : 'agent_failed';++ lines.push(line({++ ...base,++ event: 'result',++ agent: e._agent,++ ok: !failed,++ session: e.session_id,++ text: clip(e.result, MAX_TEXT),++ cost_usd: Number(e.total_cost_usd) || undefined,++ ...(failed ? { code, retry: false, suggestion: (ERRORS[code] || ERRORS.agent_failed).suggestion } : {}),++ }));++ return lines;++ }++ return lines;++}++++/** A run's own one-line header, for `show` and `runs` (not the full event stream). */++function runLine(r) {++ return line({++ event: 'run', run: r.run_id, cwd: r.cwd, task: r.task, permission: r.permission,++ agents: (r.attempts || []).map((a) => a.agent), ok: !!(r.result && !r.result.is_error),++ prompt: clip(String(r.prompt || '').split('\n')[0], MAX_REF),++ });++}++++function attemptLine(runId, a) {++ return line({++ event: 'attempt', run: runId, n: a.attempt, agent: a.agent, ok: !!a.ok,++ session: a.session_id, error: a.error ? clip(a.error, MAX_TEXT) : undefined,++ });++}++++/** `show --for-agent`: the header, each attempt, then the result (nothing else, unlike --json). */++function showLines(r) {++ const lines = [runLine(r)];++ for (const a of r.attempts || []) lines.push(attemptLine(r.run_id, a));++ if (r.result) lines.push(line({++ event: 'result', run: r.run_id, ok: !r.result.is_error, session: r.result.session_id,++ text: clip(r.result.result, MAX_TEXT),++ ...(r.result.is_error ? { code: 'agent_failed', retry: false, suggestion: ERRORS.agent_failed.suggestion } : {}),++ }));++ return lines;++}++++module.exports = { MAX_TEXT, MAX_REF, ERRORS, clip, line, errorLine, refOf, eventLines, runLine, attemptLine, showLines };@@@ -10,12 +10,15 @@ const path = require('path');const { createHarness } = require('../harness');const { runJob, resultWriter, jobEnv } = require('../harness/job');const { task } = require('./task');++const { skills } = require('./skills');++const agentOut = require('./agent-output');const { createProofAdapters } = require('../harness/proof');const { version: VERSION } = require('../package.json');const { probe: probeLogins } = require('../engine/logins');const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your terminal++ openvibe-codes skills [get|list|path] [name] the guide an agent reads, shipped with this binaryopenvibe-codes agents which agents this machine can run, and why not the othersopenvibe-codes logins the coding CLIs here: installed, signed in, and their limit windowsopenvibe-codes route [--task edit] [--need harness:resume,…] which agent a task would go to, and why@@@ -37,6 +40,9 @@ const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your term--no-handoff stop at the first agent's result--attempts <n> at most this many agents in one run (default 3)--json the events as JSON lines instead of a readable log++ --for-agent one compact JSON line per event with fixed fields (run, seq,++ event, agent, session, ref, ok, error {code,retry,suggestion});++ no ANSI, bounded text. The machine face of every command here.openvibe-codes runs the latest runs on this machineopenvibe-codes show <run id> [--json] one run's attempts and result (--json: every event)openvibe-codes resume <run id> "follow-up" continue a run's last session with its agent@@@ -52,11 +58,14 @@ Environment: DEEPSEEK_API_KEY · OPENVIBE_CODES_BASE_URL, OPENVIBE_CODES_MODEL,Network token, for 'task') and OPENVIBE_CODES_URL (the server task talks to).Guide: https://openvibe.codes/start`;++let agentMode = false; // --for-agent: the final error handler answers in the same envelope++function parse(argv) {const opts = { _: [] };for (let i = 0; i < argv.length; i++) {const a = argv[i];if (a === '--json') opts.json = true;++ else if (a === '--for-agent') { opts.forAgent = true; agentMode = true; }else if (a === '--advisor') opts.advisor = true;else if (a === '--no-handoff') opts.handoff = false;else if (a === '--help' || a === '-h') opts.help = true;@@@ -110,12 +119,17 @@ async function main() {// The Codes MCP server (Codes C9b): a conductor's Claude Code session starts it with --mcp-config.if (cmd === 'mcp') return require('./mcp').mcp();const o = parse(rest);++ if (cmd === 'skills') return skills(rest.filter((a) => a !== '--json'), { out, json: o.json });if (!cmd || cmd === 'help' || o.help || cmd === '--help' || cmd === '-h') { out(USAGE); return 0; }if (cmd === '--version' || cmd === '-v' || cmd === 'version') { out(VERSION); return 0; }const harness = createHarness();if (cmd === 'agents') {const list = harness.agents();++ if (o.forAgent) {++ for (const a of list) out(agentOut.line({ event: 'agent', id: a.id, name: a.name, available: !!a.available, reason: a.available ? undefined : a.reason }));++ return 0;++ }if (o.json) { out(JSON.stringify(list, null, 2)); return 0; }for (const a of list) out(`${a.available ? '✔' : '·'} ${a.id.padEnd(18)} ${a.name.padEnd(16)} ${a.available ? dim([a.where, a.version].filter(Boolean).join(' · ')) : dim(a.reason)}`);return 0;@@@ -124,6 +138,10 @@ async function main() {// What this machine has: each CLI, whether it is signed in, and the limit windows it can read. Nothing else:// no key, no token, no account name.const rows = probeLogins({ home: process.env.HOME });++ if (o.forAgent) {++ for (const r of rows) out(agentOut.line({ event: 'login', id: r.id, installed: !!r.installed, signed_in: !!r.signed_in, windows: r.windows || [] }));++ return 0;++ }if (o.json) { out(JSON.stringify(rows, null, 2)); return 0; }for (const r of rows) {const windows = r.windows.length ? r.windows.map((w) => `${w.name}${w.used != null ? ` ${w.used}/${w.limit}` : ''}${w.resets_at ? ` (resets ${w.resets_at})` : ''}`).join(', ') : '';@@@ -134,6 +152,7 @@ async function main() {if (cmd === 'route') {const requirements = o.need ? { capabilities: String(o.need).split(',').map((s) => s.trim()).filter(Boolean) } : {};const d = harness.route({ task: o.task || 'edit', requirements });++ if (o.forAgent) { out(agentOut.line({ event: 'route', selected: d.selected || null, reasons: d.reasons || [], candidates: (d.candidates || []).map((c) => ({ id: c.id, included: !!c.included, reason: c.reason || undefined })) })); return d.selected ? 0 : 1; }if (o.json) { out(JSON.stringify(d, null, 2)); return 0; }out(d.selected ? `→ ${d.selected}` : '→ no agent here can take it');for (const r of d.reasons || []) out(dim(` ${r}`));@@@ -147,16 +166,28 @@ async function main() {let cwd = path.resolve(o.cwd || process.cwd());if (cmd === 'resume') {const prev = harness.store.loadRun(o._[0]);-- if (!prev) { out(`no run ${o._[0]} on this machine (openvibe-codes runs)`); return 1; }++ if (!prev) {++ if (o.forAgent) out(agentOut.errorLine('no_run', `no run ${o._[0]} on this machine`));++ else out(`no run ${o._[0]} on this machine (openvibe-codes runs)`);++ return 1;++ }const last = [...(prev.attempts || [])].reverse().find((a) => a.session_id);-- if (!last) { out('that run has no session to resume'); return 1; }++ if (!last) {++ if (o.forAgent) out(agentOut.errorLine('no_run', 'that run has no session to resume', { suggestion: 'start a new run instead: `openvibe-codes run --for-agent "<task>"`' }));++ else out('that run has no session to resume');++ return 1;++ }resume = last.session_id; agent = last.agent; cwd = prev.cwd || cwd;prompt = o._.slice(1).join(' ');} else {prompt = o._.join(' ');}if (prompt === '-') prompt = await readStdin();-- if (!prompt || !prompt.trim()) { out('what should the agent do? openvibe-codes run "…"'); return 1; }++ if (!prompt || !prompt.trim()) {++ if (o.forAgent) out(agentOut.errorLine('no_prompt', 'no task was given'));++ else out('what should the agent do? openvibe-codes run "…"');++ return 1;++ }const controller = new AbortController();process.on('SIGINT', () => { controller.abort(); });const run = harness.run({@@@ -164,9 +195,15 @@ async function main() {permission: o.permission || 'edit', handoff: o.handoff !== false && !resume, maxAttempts: Number(o.attempts) || 3, signal: controller.signal,effort: o.effort || null, advisor: o.advisor ? (o['advisor-model'] ? { model: o['advisor-model'] } : true) : null,});-- if (!o.json) out(dim(`run ${run.runId} · ${cwd}`));++ if (!o.json && !o.forAgent) out(dim(`run ${run.runId} · ${cwd}`));let last = null;-- for await (const e of run) { last = e; if (o.json) out(JSON.stringify(e)); else printEvent(e); }++ let seq = 0;++ for await (const e of run) {++ last = e;++ if (o.forAgent) { for (const l of agentOut.eventLines(run.runId, seq + 1, e)) { seq++; out(l); } }++ else if (o.json) out(JSON.stringify(e));++ else printEvent(e);++ }return last && last.type === 'result' && !last.is_error ? 0 : 1;}if (cmd === 'job') {@@@ -194,6 +231,7 @@ async function main() {}if (cmd === 'runs') {const runs = harness.store.listRuns(Number(o.limit) || 20);++ if (o.forAgent) { for (const r of runs) out(agentOut.runLine(r)); return 0; }if (o.json) { out(JSON.stringify(runs, null, 2)); return 0; }for (const r of runs) {const ok = r.result && !r.result.is_error;@@@ -203,15 +241,25 @@ async function main() {}if (cmd === 'show') {const r = harness.store.loadRun(o._[0]);-- if (!r) { out(`no run ${o._[0]} on this machine`); return 1; }++ if (!r) {++ if (o.forAgent) out(agentOut.errorLine('no_run', `no run ${o._[0]} on this machine`));++ else out(`no run ${o._[0]} on this machine`);++ return 1;++ }++ if (o.forAgent) { for (const l of agentOut.showLines(r)) out(l); return r.result && !r.result.is_error ? 0 : 1; }if (o.json) { for (const e of harness.store.events(r.run_id)) out(JSON.stringify(e)); return 0; }out(`${r.run_id} · ${r.cwd} · ${r.permission}`);for (const a of r.attempts || []) out(` ${a.ok ? '✔' : '✗'} ${a.agent}${a.session_id ? dim(` session ${a.session_id}`) : ''}${a.error ? ` — ${a.error}` : ''}`);if (r.result) out(r.result.result || '');return r.result && !r.result.is_error ? 0 : 1;}++ if (o.forAgent) { out(agentOut.errorLine('usage', `unknown command or option: ${[cmd, ...rest].filter(Boolean).join(' ')}`)); return 1; }out(USAGE);return 1;}--main().then((code) => process.exit(code), (err) => { process.stderr.write(`openvibe-codes: ${err.message}\n`); process.exit(1); });++main().then((code) => process.exit(code), (err) => {++ if (agentMode) process.stdout.write(`${agentOut.errorLine('internal', err && err.message)}\n`);++ process.stderr.write(`openvibe-codes: ${err && err.message}\n`);++ process.exit(1);++});nnew file mode 100644@@@ -0,0 +1,77 @@++'use strict';++++/**++ * `openvibe-codes skills`: the guide an agent reads instead of a human pasting the CLI reference into a prompt.++ *++ * The guide is a file shipped inside the package, so it is version-matched by construction: `{{version}}` in the file++ * is replaced with this binary's own version, and a test fails if the guide and the CLI's flags drift apart. An agent++ * loads it on demand:++ *++ * openvibe-codes skills list # the guides this binary ships++ * openvibe-codes skills get # the openvibe-codes guide (Markdown, on stdout)++ * openvibe-codes skills get openvibe-codes # the same, named++ * openvibe-codes skills path # where the file is, for a host that loads files itself++ */++++const fs = require('fs');++const path = require('path');++const { version: VERSION } = require('../package.json');++++const DIR = path.join(__dirname, 'skills');++++// One guide today. Aliases keep `skills get codes` working; a second guide is one entry here and one file.++const GUIDES = [++ { name: 'openvibe-codes', aliases: ['codes'], file: 'openvibe-codes.md', description: 'run coding agents from a script or another agent: commands, --for-agent lines, exit codes, error codes, resume' },++];++++const byName = (name) => GUIDES.find((g) => g.name === name || (g.aliases || []).includes(name)) || null;++++/** Every guide this binary ships: its name, version, description and file. */++function list() {++ return GUIDES.map((g) => ({ name: g.name, version: VERSION, description: g.description, path: path.join(DIR, g.file) }));++}++++/** One guide's Markdown, with the version this binary reports put in (it always matches the CLI it came from). */++function read(name = 'openvibe-codes') {++ const g = byName(name);++ if (!g) return null;++ const file = path.join(DIR, g.file);++ let text;++ try { text = fs.readFileSync(file, 'utf8'); } catch { return null; }++ return text.replace(/\{\{version\}\}/g, VERSION);++}++++/**++ * The `skills` command. `out` is stdout's writer; the return value is the exit code.++ *++ * @param {string[]} argv words after `skills`++ * @param {{out: (s: string) => void, json?: boolean}} io++ */++function skills(argv, { out, json = false } = {}) {++ const [sub = 'list', name] = argv;++ if (sub === 'list') {++ const rows = list();++ if (json) { out(JSON.stringify(rows)); return 0; }++ for (const r of rows) out(`${r.name} ${r.version} ${r.description}`);++ return 0;++ }++ if (sub === 'get') {++ const text = read(name || 'openvibe-codes');++ if (text == null) {++ out(`no guide named ${name || ''} (openvibe-codes skills list)`);++ return 1;++ }++ out(text.trimEnd());++ return 0;++ }++ if (sub === 'path') {++ const g = byName(name || 'openvibe-codes');++ if (!g) { out(`no guide named ${name || ''} (openvibe-codes skills list)`); return 1; }++ out(path.join(DIR, g.file));++ return 0;++ }++ out(`unknown skills command ${sub} (openvibe-codes skills list|get|path [name])`);++ return 1;++}++++module.exports = { GUIDES, list, read, skills, byName };nnew file mode 100644@@@ -0,0 +1,125 @@++# openvibe-codes — the guide for agents++++Version: {{version}} (shipped inside the binary; it always matches `openvibe-codes --version`)++++`openvibe-codes` runs a coding task on an agent CLI installed on **this machine** — Claude Code, Codex, OpenCode,++Command Code, MiniMax Code or Aider — or on the DeepSeek API or any OpenAI-compatible server. The harness routes the++task, hands it to the next agent when one fails, and keeps every run. Nothing is sent to OpenVibe.++++Use this guide when you are an agent driving `openvibe-codes` (or a script that supervises one). Read it with++`openvibe-codes skills get`; do not paste the human documentation into a prompt.++++## First, what is here++++```bash++openvibe-codes agents --for-agent # one line: which harness ids this machine can run, and why not the rest++openvibe-codes logins --for-agent # installed, signed in, each provider's limit windows++openvibe-codes route --for-agent --task edit # which agent a task would go to, and why++```++++Only run an id that `agents --for-agent` reports `"available":true`. An agent that is not installed, not signed in++or at its usage limit will not be picked.++++## Run a task++++```bash++openvibe-codes run --for-agent "rename getUser to findUser"++openvibe-codes run --for-agent --agent codex --permission read "where is the session cookie set?"++openvibe-codes run --for-agent --task review "review the diff on this branch"++openvibe-codes run --for-agent - < brief.md # the task on stdin++```++++- `--agent <id>` skips routing and runs that harness first. Without it the harness picks, cheapest suitable first,++ and hands over on failure.++- `--permission read|edit|full` — `read` looks only, `edit` (default) changes files, `full` is anything unattended.++ OpenCode and Command Code have no edits-only mode; outside a sandbox they need `full`.++- `--model`, `--effort low|medium|high|max`, `--advisor` are passed to the first agent.++- `--cwd <dir>` is where the agent works (default: the current directory).++- `--no-handoff` stops at the first agent's result; `--attempts <n>` caps the agents in one run (default 3).++- `--json` is the full event stream for a person debugging; **use `--for-agent` instead**.++++## The `--for-agent` envelope++++One compact JSON object per line on stdout, no ANSI and no pretty printing. Every line has fixed fields:++++| field | meaning |++|---|---|++| `run` | the run id; `show` and `resume` take it |++| `seq` | the line's number in this run, from 1 |++| `event` | `init`, `text`, `tool`, `tool_error`, `progress`, `handoff`, `warning`, `stderr`, `result`; `show`/`runs` also answer `run`, `attempt`, `error` |++| `agent` | the harness that produced the line |++| `session` | the harness's session id, when it has one (what `resume` continues) |++| `ref` | what a tool call points at: a file, a pattern, a URL or the first line of a command |++| `ok` | on a result: did the agent finish without an error |++| `error` | on a failure: `{ code, retry, suggestion }` |++| `text` | the message or answer, bounded to 400 characters |++++A successful run ends with exactly one `result` line with `"ok":true`. A failed run ends with one `result` line++carrying `"ok":false` and a `code`; a failure before any agent ran is one `error` line. Text is truncated to keep an++agent's context small; the human log (no `--for-agent`) has all of it.++++Example lines:++++```json++{"run":"run_01J…","seq":1,"event":"init","agent":"codex","model":"gpt-6","session":"cx-1"}++{"run":"run_01J…","seq":2,"event":"tool","agent":"codex","tool":"Edit","ref":"src/user.js"}++{"run":"run_01J…","seq":3,"event":"result","agent":"codex","ok":true,"session":"cx-1","text":"renamed getUser to findUser","cost_usd":0.02}++```++++## Reading a run without re-running it++++```bash++openvibe-codes runs --for-agent # one line per kept run on this machine++openvibe-codes show <run> --for-agent # the header, each attempt, the result (not every event)++openvibe-codes resume <run> --for-agent "now add the test" # continue the last session with the same agent++```++++`show --json` dumps every event; an agent should use `show --for-agent`, which is bounded.++++## Errors and retries++++Failures carry `{code, retry, suggestion}`. Branch on `code`, never on the message text.++++| code | retry | what to do |++|---|---|---|++| `no_prompt` | false | give the task as an argument, or `-` plus stdin |++| `bad_agent` | false | the id is not in the catalog: read `agents --for-agent` |++| `unavailable` | false | the harness is not installed or signed in here: read `agents --for-agent` |++| `agent_failed` | false | read the result, then `resume` the same session with what is missing |++| `no_run` | false | the id is not on this machine: read `runs --for-agent` |++| `usage` | false | a wrong command line: read `skills get` |++| `internal` | false | an unexpected error: run the command without `--for-agent` for the full message |++++`retry` is `true` only when repeating the exact same command is safe. An agent run is not idempotent (it edits the++working tree), so a failed run is continued with `resume`, not started again.++++## Exit codes++++| code | meaning |++|---|---|++| 0 | the last agent finished without an error |++| 1 | a failure: the task failed, no agent could take it, or the command line was wrong |++| 2 | `task` only: the command line or the Network token is wrong (nothing was sent) |++++## Tasks on openvibe.codes++++`openvibe-codes task` hands a task to openvibe.codes/run and one of the person's machines runs it there:++++```bash++openvibe-codes task start --repo OpenVibers/OpenVibe.Codes --file brief.md --name codes-c6 --tier hard++openvibe-codes task list --state running|needs|done++openvibe-codes task show <task> --follow++openvibe-codes task continue <task> --note "and add the test the reviewer asked for"++openvibe-codes task stop <task>++```++++It needs a personal Network token (`OPENVIBE_TOKEN`, or `~/.config/openvibe/token`). `docs/cli.md` in the repository++has its flags and the task API it maps onto.++++## The rules of this machine++++- Do not parse the human log: use `--for-agent`.++- Do not assume an agent's tool names: `tool` and `ref` are normalised to Claude Code's vocabulary.++- Do not repeat a mutating run; `resume` it with a follow-up.++- The harness never prints a token, and neither should you: `openvibe-codes` scrubs secrets out of events.++- The run is kept in `OPENVIBE_CODES_HOME` (else `$XDG_STATE_HOME/openvibe-codes`, else `~/.local/state/openvibe-codes`).@@@ -52,6 +52,33 @@ openvibe-codes task stop tsk_01J…`GET /api/v1/tasks/:id/events` and prints the session's lines, each attempt and why it happened, ending when theoutcome is decided.++## For an agent: `--for-agent` and `skills get`++++A supervising agent should not read the human log. `--for-agent` is the machine face of every command in this++document that runs or reads a run: one compact JSON object per line, no ANSI and no pretty printing, with fixed++fields — `run`, `seq`, `event`, `agent`, `session`, `ref`, `ok`, and on a failure `error: {code, retry,++suggestion}`. Text is bounded to 400 characters; `--json` stays the full event stream for a person debugging.++++```bash++openvibe-codes agents --for-agent # one line per harness, with why it cannot run++openvibe-codes run --for-agent "rename getUser to findUser"++openvibe-codes runs --for-agent++openvibe-codes show <run id> --for-agent # the header, each attempt, the result (not every event)++openvibe-codes resume <run id> --for-agent "now add the test"++```++++`retry` is true only when repeating the exact command is safe; an agent run is not idempotent, so a failed run is++continued with `resume`. The closed set of error codes is in the guide the binary ships:++++```bash++openvibe-codes skills list # the guides inside this binary++openvibe-codes skills get # the openvibe-codes guide, Markdown, for the exact version you are running++openvibe-codes skills path # where the file is, for a host that loads files itself++```++++The guide is version-matched by construction: it is in the package and its `{{version}}` line is replaced with the++binary's own version, and a test keeps it and the CLI's flags in step.++## Exit codes| code | meaning |nnew file mode 100644@@@ -0,0 +1,108 @@++'use strict';++/**++ * The agent face of `openvibe-codes`: `--for-agent` (one compact JSON line per event, fixed fields, typed errors) and++ * `skills get` (the version-matched guide shipped inside the binary). The human log and --json stay as they were.++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { spawnSync } = require('child_process');++const { version } = require('../package.json');++const { fakeAgents } = require('./helpers/fake-agents');++const { check, done } = require('./helpers/boot');++++const BIN = path.join(__dirname, '..', 'bin', 'openvibe-codes.js');++++(async () => {++ const f = fakeAgents({ codex: 'ok' });++ const cli = (...args) => {++ const r = spawnSync(process.execPath, [BIN, ...args], { cwd: f.work, env: { ...process.env, ...f.env }, encoding: 'utf8', timeout: 60_000 });++ return { code: r.status, out: r.stdout, err: r.stderr };++ };++ const lines = (out) => out.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));++++ await check('skills get: the guide is shipped with this binary, at its version', () => {++ const r = cli('skills', 'get');++ assert.strictEqual(r.code, 0, r.err);++ assert.match(r.out, new RegExp(`Version: ${version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));++ assert.match(r.out, /--for-agent/);++ assert.match(r.out, /skills get/);++ assert.ok(!r.out.includes('{{version}}'), 'the version placeholder is replaced');++ const list = JSON.parse(cli('skills', 'list', '--json').out);++ assert.deepStrictEqual(list.map((g) => g.name), ['openvibe-codes']);++ assert.strictEqual(list[0].version, version);++ const where = cli('skills', 'path').out.trim();++ assert.ok(fs.existsSync(where), where);++ assert.strictEqual(cli('skills', 'get', 'nope').code, 1);++ });++++ await check('run --for-agent: one compact JSON line per event, a fixed envelope and a bounded result', () => {++ const r = cli('run', '--agent', 'codex', '--no-handoff', '--for-agent', 'x'.repeat(1200));++ assert.strictEqual(r.code, 0, r.out + r.err);++ const rows = lines(r.out);++ assert.ok(rows.length >= 3, r.out);++ assert.strictEqual(rows[0].event, 'init');++ assert.strictEqual(rows[0].agent, 'codex');++ assert.ok(rows[0].run, 'the run id is on every line');++ assert.deepStrictEqual(rows.map((x) => x.seq), rows.map((_, i) => i + 1), 'seq counts the lines');++ assert.ok(rows.every((x) => x.run === rows[0].run));++ assert.ok(rows.some((x) => x.event === 'tool' && x.ref === 'a.js'), r.out);++ const result = rows[rows.length - 1];++ assert.strictEqual(result.event, 'result');++ assert.strictEqual(result.ok, true);++ assert.ok(result.text.length <= 400, 'the answer is bounded');++ assert.ok(!/\x1b\[/.test(r.out), 'no ANSI in the agent output');++ assert.ok(!r.out.includes('▶'), 'not the human log');++ });++++ await check('a failed run ends with {code, retry, suggestion}; a missing agent is unavailable', () => {++ f.env.FAKE_CODEX = 'fail';++ const r = cli('run', '--agent', 'codex', '--no-handoff', '--for-agent', 'x');++ assert.strictEqual(r.code, 1);++ const result = lines(r.out).pop();++ assert.strictEqual(result.event, 'result');++ assert.strictEqual(result.ok, false);++ assert.strictEqual(result.code, 'agent_failed');++ assert.strictEqual(result.retry, false);++ assert.match(result.suggestion, /resume/);++ f.env.FAKE_CODEX = 'ok';++++ const missing = cli('run', '--agent', 'not-an-agent', '--for-agent', 'x');++ assert.strictEqual(missing.code, 1);++ const refused = lines(missing.out).pop();++ assert.strictEqual(refused.code, 'unavailable');++ assert.strictEqual(refused.retry, false);++ assert.match(refused.suggestion, /agents --for-agent/);++++ const none = cli('run', '--for-agent');++ assert.strictEqual(none.code, 1);++ assert.strictEqual(lines(none.out)[0].code, 'no_prompt');++ });++++ await check('show, runs, agents and route answer the same envelope', () => {++ const run = cli('run', '--agent', 'codex', '--no-handoff', '--for-agent', 'hello');++ const runId = lines(run.out)[0].run;++ const show = cli('show', runId, '--for-agent');++ assert.strictEqual(show.code, 0, show.out + show.err);++ const rows = lines(show.out);++ assert.deepStrictEqual(rows.map((x) => x.event), ['run', 'attempt', 'result']);++ assert.strictEqual(rows[0].run, runId);++ assert.strictEqual(rows[2].ok, true);++ assert.strictEqual(cli('show', 'run_nope', '--for-agent').code, 1);++++ const runs = lines(cli('runs', '--for-agent').out);++ assert.ok(runs.some((x) => x.run === runId && x.event === 'run'));++++ const agents = lines(cli('agents', '--for-agent').out);++ assert.ok(agents.find((a) => a.id === 'codex' && a.available === true));++ assert.ok(agents.find((a) => a.id === 'aider' && a.available === false && a.reason));++++ const route = lines(cli('route', '--task', 'review', '--for-agent').out);++ assert.strictEqual(route.length, 1);++ assert.strictEqual(route[0].event, 'route');++ assert.ok(route[0].selected);++ });++++ f.cleanup();++ done();++})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
+'use strict';
+
+/**
+ * `--for-agent`: the machine face of `openvibe-codes`.
+ *
+ * A supervising agent asks the CLI with `--for-agent` and reads one compact JSON object per line: no ANSI, no pretty
+ * printing, no full event dump. The fields are fixed so a caller can branch on them without parsing words:
+ *
+ * run the run id (the same one `show` and `resume` take)
+ * seq the line's number in this run, from 1
+ * event init | text | tool | tool_error | progress | handoff | warning | stderr | result | run | attempt | error
+ * agent the harness that produced the line (init, result and the run summaries)
+ * session the harness's session id, when it has one (the key `resume` needs)
+ * ref what a tool call points at: a file, a pattern, a URL or the first line of a command
+ * ok on a result: did the agent finish without an error
+ * error on a failure: { code, retry, suggestion }; retry is true only when repeating the same command is safe
+ *
+ * Text is bounded (MAX_TEXT) because a supervising agent pays for every token of it: the human log stays the full one.
+ * The guide that explains these fields ships with the binary and matches its version: `openvibe-codes skills get`.
+ */
+
+const MAX_TEXT = 400;
+const MAX_REF = 200;
+
+// The closed set of codes. A caller branches on the code, never on the message.
+const ERRORS = {
+ no_prompt: { retry: false, suggestion: 'pass the task as an argument, or "-" and write it on stdin' },
+ bad_agent: { retry: false, suggestion: 'run `openvibe-codes agents --for-agent` and pick an id it lists as available' },
+ unavailable: { retry: false, suggestion: 'run `openvibe-codes agents --for-agent`; the harness is not installed or signed in here' },
+ agent_failed: { retry: false, suggestion: 'read the result, then continue the same session with `openvibe-codes resume <run> "<follow-up>"`' },
+ no_run: { retry: false, suggestion: 'run `openvibe-codes runs --for-agent` and use an id from it' },
+ usage: { retry: false, suggestion: 'run `openvibe-codes skills get` for the command and output reference' },
+ internal: { retry: false, suggestion: 'run the same command without --for-agent to see the full error' },
+};
+
+const clip = (s, n) => { const t = String(s == null ? '' : s); return t.length > n ? `${t.slice(0, n - 1)}…` : t; };
+
+/** One JSON line. `undefined` fields are left out, so the envelope stays small. */
+function line(doc) {
+ const out = {};
+ for (const [k, v] of Object.entries(doc)) if (v !== undefined && v !== null && v !== '') out[k] = v;
+ return JSON.stringify(out);
+}
+
+/** A failure the caller can act on: a code, whether repeating the command is safe, and what to do. */
+function errorLine(code, message, extra = {}) {
+ const known = ERRORS[code] || ERRORS.internal;
+ return line({
+ event: 'error', code, retry: extra.retry != null ? !!extra.retry : known.retry,
+ suggestion: extra.suggestion || known.suggestion, message: clip(message, MAX_TEXT),
+ ...(extra.run ? { run: extra.run } : {}), ...(extra.seq ? { seq: extra.seq } : {}),
+ });
+}
+
+/** What a tool call points at, in the harness's own vocabulary (Claude Code's names are the shared ones). */
+function refOf(input) {
+ const i = input && typeof input === 'object' ? input : {};
+ for (const key of ['file_path', 'path', 'notebook_path', 'pattern', 'query', 'url', 'command', 'description']) {
+ const v = i[key];
+ if (typeof v === 'string' && v.trim()) return clip(v.split('\n')[0].trim(), MAX_REF);
+ }
+ return undefined;
+}
+
+/** The lines one harness event becomes. An assistant message may hold text and tool calls: one line each. */
+function eventLines(runId, seq, e) {
+ if (!e || typeof e !== 'object') return [];
+ const base = { run: runId, ...(seq == null ? {} : { seq }) };
+ const lines = [];
+ if (e.type === 'system') {
+ if (e.subtype === 'init') lines.push(line({ ...base, event: 'init', agent: e.agent, model: e.model, session: e.session_id }));
+ else if (e.subtype === 'handoff') lines.push(line({ ...base, event: 'handoff', from: e.from, reason: clip(e.reason, MAX_TEXT) }));
+ else if (e.subtype === 'warning' || e.subtype === 'error') lines.push(line({ ...base, event: 'warning', level: e.subtype, message: clip(e.text, MAX_TEXT) }));
+ else if (e.subtype === 'stderr') lines.push(line({ ...base, event: 'stderr', message: clip(e.text, MAX_TEXT) }));
+ else if (e.subtype === 'tool_progress') lines.push(line({ ...base, event: 'progress', tool: e.tool_use_id, text: clip(e.text, MAX_TEXT) }));
+ return lines;
+ }
+ if (e.type === 'assistant') {
+ for (const c of (e.message && e.message.content) || []) {
+ if (c.type === 'text' && String(c.text || '').trim()) lines.push(line({ ...base, event: 'text', agent: e._agent, text: clip(c.text, MAX_TEXT) }));
+ else if (c.type === 'tool_use') lines.push(line({ ...base, event: 'tool', agent: e._agent, tool: c.name, ref: refOf(c.input) }));
+ }
+ return lines;
+ }
+ if (e.type === 'user') {
+ for (const c of (e.message && e.message.content) || []) {
+ if (c.type === 'tool_result' && c.is_error) lines.push(line({ ...base, event: 'tool_error', text: clip(c.content, MAX_TEXT) }));
+ }
+ return lines;
+ }
+ if (e.type === 'result') {
+ const failed = !!e.is_error;
+ // "X is not available here" is the harness refusing before an agent ran, not an agent's own failure.
+ const code = failed && /is not available here/.test(String(e.result || '')) ? 'unavailable' : 'agent_failed';
+ lines.push(line({
+ ...base,
+ event: 'result',
+ agent: e._agent,
+ ok: !failed,
+ session: e.session_id,
+ text: clip(e.result, MAX_TEXT),
+ cost_usd: Number(e.total_cost_usd) || undefined,
+ ...(failed ? { code, retry: false, suggestion: (ERRORS[code] || ERRORS.agent_failed).suggestion } : {}),
+ }));
+ return lines;
+ }
+ return lines;
+}
+
+/** A run's own one-line header, for `show` and `runs` (not the full event stream). */
+function runLine(r) {
+ return line({
+ event: 'run', run: r.run_id, cwd: r.cwd, task: r.task, permission: r.permission,
+ agents: (r.attempts || []).map((a) => a.agent), ok: !!(r.result && !r.result.is_error),
+ prompt: clip(String(r.prompt || '').split('\n')[0], MAX_REF),
+ });
+}
+
+function attemptLine(runId, a) {
+ return line({
+ event: 'attempt', run: runId, n: a.attempt, agent: a.agent, ok: !!a.ok,
+ session: a.session_id, error: a.error ? clip(a.error, MAX_TEXT) : undefined,
+ });
+}
+
+/** `show --for-agent`: the header, each attempt, then the result (nothing else, unlike --json). */
+function showLines(r) {
+ const lines = [runLine(r)];
+ for (const a of r.attempts || []) lines.push(attemptLine(r.run_id, a));
+ if (r.result) lines.push(line({
+ event: 'result', run: r.run_id, ok: !r.result.is_error, session: r.result.session_id,
+ text: clip(r.result.result, MAX_TEXT),
+ ...(r.result.is_error ? { code: 'agent_failed', retry: false, suggestion: ERRORS.agent_failed.suggestion } : {}),
+ }));
+ return lines;
+}
+
+module.exports = { MAX_TEXT, MAX_REF, ERRORS, clip, line, errorLine, refOf, eventLines, runLine, attemptLine, showLines };
@@ -10,12 +10,15 @@ const path = require('path');
@@ -10,12 +10,15 @@ const path = require('path');
const { createHarness } = require('../harness');
const { createHarness } = require('../harness');
const { runJob, resultWriter, jobEnv } = require('../harness/job');
const { runJob, resultWriter, jobEnv } = require('../harness/job');
const { task } = require('./task');
const { task } = require('./task');
+const { skills } = require('./skills');
+const agentOut = require('./agent-output');
const { createProofAdapters } = require('../harness/proof');
const { createProofAdapters } = require('../harness/proof');
const { version: VERSION } = require('../package.json');
const { version: VERSION } = require('../package.json');
const { probe: probeLogins } = require('../engine/logins');
const { probe: probeLogins } = require('../engine/logins');
const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your terminal
const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your terminal
+ openvibe-codes skills [get|list|path] [name] the guide an agent reads, shipped with this binary
openvibe-codes agents which agents this machine can run, and why not the others
openvibe-codes agents which agents this machine can run, and why not the others
openvibe-codes logins the coding CLIs here: installed, signed in, and their limit windows
openvibe-codes logins the coding CLIs here: installed, signed in, and their limit windows
openvibe-codes route [--task edit] [--need harness:resume,…] which agent a task would go to, and why
openvibe-codes route [--task edit] [--need harness:resume,…] which agent a task would go to, and why
@@ -37,6 +40,9 @@ const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your term
@@ -37,6 +40,9 @@ const USAGE = `openvibe-codes ${VERSION} — code with any agent, from your term
--no-handoff stop at the first agent's result
--no-handoff stop at the first agent's result
--attempts <n> at most this many agents in one run (default 3)
--attempts <n> at most this many agents in one run (default 3)
--json the events as JSON lines instead of a readable log
--json the events as JSON lines instead of a readable log
+ --for-agent one compact JSON line per event with fixed fields (run, seq,
+ event, agent, session, ref, ok, error {code,retry,suggestion});
+ no ANSI, bounded text. The machine face of every command here.
openvibe-codes runs the latest runs on this machine
openvibe-codes runs the latest runs on this machine
openvibe-codes show <run id> [--json] one run's attempts and result (--json: every event)
openvibe-codes show <run id> [--json] one run's attempts and result (--json: every event)
openvibe-codes resume <run id> "follow-up" continue a run's last session with its agent
openvibe-codes resume <run id> "follow-up" continue a run's last session with its agent
@@ -52,11 +58,14 @@ Environment: DEEPSEEK_API_KEY · OPENVIBE_CODES_BASE_URL, OPENVIBE_CODES_MODEL,
@@ -52,11 +58,14 @@ Environment: DEEPSEEK_API_KEY · OPENVIBE_CODES_BASE_URL, OPENVIBE_CODES_MODEL,
Network token, for 'task') and OPENVIBE_CODES_URL (the server task talks to).
Network token, for 'task') and OPENVIBE_CODES_URL (the server task talks to).
Guide: https://openvibe.codes/start`;
Guide: https://openvibe.codes/start`;
+let agentMode = false; // --for-agent: the final error handler answers in the same envelope
+
function parse(argv) {
function parse(argv) {
const opts = { _: [] };
const opts = { _: [] };
for (let i = 0; i < argv.length; i++) {
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const a = argv[i];
if (a === '--json') opts.json = true;
if (a === '--json') opts.json = true;
+ else if (a === '--for-agent') { opts.forAgent = true; agentMode = true; }
else if (a === '--advisor') opts.advisor = true;
else if (a === '--advisor') opts.advisor = true;
else if (a === '--no-handoff') opts.handoff = false;
else if (a === '--no-handoff') opts.handoff = false;
else if (a === '--help' || a === '-h') opts.help = true;
else if (a === '--help' || a === '-h') opts.help = true;
@@ -110,12 +119,17 @@ async function main() {
@@ -110,12 +119,17 @@ async function main() {
// The Codes MCP server (Codes C9b): a conductor's Claude Code session starts it with --mcp-config.
// The Codes MCP server (Codes C9b): a conductor's Claude Code session starts it with --mcp-config.
if (cmd === 'mcp') return require('./mcp').mcp();
if (cmd === 'mcp') return require('./mcp').mcp();
const o = parse(rest);
const o = parse(rest);
+ if (cmd === 'skills') return skills(rest.filter((a) => a !== '--json'), { out, json: o.json });
if (!cmd || cmd === 'help' || o.help || cmd === '--help' || cmd === '-h') { out(USAGE); return 0; }
if (!cmd || cmd === 'help' || o.help || cmd === '--help' || cmd === '-h') { out(USAGE); return 0; }
if (cmd === '--version' || cmd === '-v' || cmd === 'version') { out(VERSION); return 0; }
if (cmd === '--version' || cmd === '-v' || cmd === 'version') { out(VERSION); return 0; }
const harness = createHarness();
const harness = createHarness();
if (cmd === 'agents') {
if (cmd === 'agents') {
const list = harness.agents();
const list = harness.agents();
+ if (o.forAgent) {
+ for (const a of list) out(agentOut.line({ event: 'agent', id: a.id, name: a.name, available: !!a.available, reason: a.available ? undefined : a.reason }));
+ return 0;
+ }
if (o.json) { out(JSON.stringify(list, null, 2)); return 0; }
if (o.json) { out(JSON.stringify(list, null, 2)); return 0; }
for (const a of list) out(`${a.available ? '✔' : '·'} ${a.id.padEnd(18)} ${a.name.padEnd(16)} ${a.available ? dim([a.where, a.version].filter(Boolean).join(' · ')) : dim(a.reason)}`);
for (const a of list) out(`${a.available ? '✔' : '·'} ${a.id.padEnd(18)} ${a.name.padEnd(16)} ${a.available ? dim([a.where, a.version].filter(Boolean).join(' · ')) : dim(a.reason)}`);
return 0;
return 0;
@@ -124,6 +138,10 @@ async function main() {
@@ -124,6 +138,10 @@ async function main() {
// What this machine has: each CLI, whether it is signed in, and the limit windows it can read. Nothing else:
// What this machine has: each CLI, whether it is signed in, and the limit windows it can read. Nothing else:
// no key, no token, no account name.
// no key, no token, no account name.
const rows = probeLogins({ home: process.env.HOME });
const rows = probeLogins({ home: process.env.HOME });
+ if (o.forAgent) {
+ for (const r of rows) out(agentOut.line({ event: 'login', id: r.id, installed: !!r.installed, signed_in: !!r.signed_in, windows: r.windows || [] }));
+ return 0;
+ }
if (o.json) { out(JSON.stringify(rows, null, 2)); return 0; }
if (o.json) { out(JSON.stringify(rows, null, 2)); return 0; }
for (const r of rows) {
for (const r of rows) {
const windows = r.windows.length ? r.windows.map((w) => `${w.name}${w.used != null ? ` ${w.used}/${w.limit}` : ''}${w.resets_at ? ` (resets ${w.resets_at})` : ''}`).join(', ') : '';
const windows = r.windows.length ? r.windows.map((w) => `${w.name}${w.used != null ? ` ${w.used}/${w.limit}` : ''}${w.resets_at ? ` (resets ${w.resets_at})` : ''}`).join(', ') : '';
@@ -134,6 +152,7 @@ async function main() {
@@ -134,6 +152,7 @@ async function main() {
if (cmd === 'route') {
if (cmd === 'route') {
const requirements = o.need ? { capabilities: String(o.need).split(',').map((s) => s.trim()).filter(Boolean) } : {};
const requirements = o.need ? { capabilities: String(o.need).split(',').map((s) => s.trim()).filter(Boolean) } : {};
const d = harness.route({ task: o.task || 'edit', requirements });
const d = harness.route({ task: o.task || 'edit', requirements });
+ if (o.forAgent) { out(agentOut.line({ event: 'route', selected: d.selected || null, reasons: d.reasons || [], candidates: (d.candidates || []).map((c) => ({ id: c.id, included: !!c.included, reason: c.reason || undefined })) })); return d.selected ? 0 : 1; }
if (o.json) { out(JSON.stringify(d, null, 2)); return 0; }
if (o.json) { out(JSON.stringify(d, null, 2)); return 0; }
out(d.selected ? `→ ${d.selected}` : '→ no agent here can take it');
out(d.selected ? `→ ${d.selected}` : '→ no agent here can take it');
for (const r of d.reasons || []) out(dim(` ${r}`));
for (const r of d.reasons || []) out(dim(` ${r}`));
@@ -147,16 +166,28 @@ async function main() {
@@ -147,16 +166,28 @@ async function main() {
let cwd = path.resolve(o.cwd || process.cwd());
let cwd = path.resolve(o.cwd || process.cwd());
if (cmd === 'resume') {
if (cmd === 'resume') {
const prev = harness.store.loadRun(o._[0]);
const prev = harness.store.loadRun(o._[0]);
- if (!prev) { out(`no run ${o._[0]} on this machine (openvibe-codes runs)`); return 1; }
+ if (!prev) {
+ if (o.forAgent) out(agentOut.errorLine('no_run', `no run ${o._[0]} on this machine`));
+ else out(`no run ${o._[0]} on this machine (openvibe-codes runs)`);
+ return 1;
+ }
const last = [...(prev.attempts || [])].reverse().find((a) => a.session_id);
const last = [...(prev.attempts || [])].reverse().find((a) => a.session_id);
- if (!last) { out('that run has no session to resume'); return 1; }
+ if (!last) {
+ if (o.forAgent) out(agentOut.errorLine('no_run', 'that run has no session to resume', { suggestion: 'start a new run instead: `openvibe-codes run --for-agent "<task>"`' }));
+ else out('that run has no session to resume');
+ return 1;
+ }
resume = last.session_id; agent = last.agent; cwd = prev.cwd || cwd;
resume = last.session_id; agent = last.agent; cwd = prev.cwd || cwd;
prompt = o._.slice(1).join(' ');
prompt = o._.slice(1).join(' ');
} else {
} else {
prompt = o._.join(' ');
prompt = o._.join(' ');
}
}
if (prompt === '-') prompt = await readStdin();
if (prompt === '-') prompt = await readStdin();
- if (!prompt || !prompt.trim()) { out('what should the agent do? openvibe-codes run "…"'); return 1; }
+ if (!prompt || !prompt.trim()) {
+ if (o.forAgent) out(agentOut.errorLine('no_prompt', 'no task was given'));
+ else out('what should the agent do? openvibe-codes run "…"');
+ return 1;
+ }
const controller = new AbortController();
const controller = new AbortController();
process.on('SIGINT', () => { controller.abort(); });
process.on('SIGINT', () => { controller.abort(); });
const run = harness.run({
const run = harness.run({
@@ -164,9 +195,15 @@ async function main() {
@@ -164,9 +195,15 @@ async function main() {
permission: o.permission || 'edit', handoff: o.handoff !== false && !resume, maxAttempts: Number(o.attempts) || 3, signal: controller.signal,
permission: o.permission || 'edit', handoff: o.handoff !== false && !resume, maxAttempts: Number(o.attempts) || 3, signal: controller.signal,
effort: o.effort || null, advisor: o.advisor ? (o['advisor-model'] ? { model: o['advisor-model'] } : true) : null,
effort: o.effort || null, advisor: o.advisor ? (o['advisor-model'] ? { model: o['advisor-model'] } : true) : null,
});
});
- if (!o.json) out(dim(`run ${run.runId} · ${cwd}`));
+ if (!o.json && !o.forAgent) out(dim(`run ${run.runId} · ${cwd}`));
let last = null;
let last = null;
- for await (const e of run) { last = e; if (o.json) out(JSON.stringify(e)); else printEvent(e); }
+ let seq = 0;
+ for await (const e of run) {
+ last = e;
+ if (o.forAgent) { for (const l of agentOut.eventLines(run.runId, seq + 1, e)) { seq++; out(l); } }
+ else if (o.json) out(JSON.stringify(e));
+ else printEvent(e);
+ }
return last && last.type === 'result' && !last.is_error ? 0 : 1;
return last && last.type === 'result' && !last.is_error ? 0 : 1;
}
}
if (cmd === 'job') {
if (cmd === 'job') {
@@ -194,6 +231,7 @@ async function main() {
@@ -194,6 +231,7 @@ async function main() {
}
}
if (cmd === 'runs') {
if (cmd === 'runs') {
const runs = harness.store.listRuns(Number(o.limit) || 20);
const runs = harness.store.listRuns(Number(o.limit) || 20);
+ if (o.forAgent) { for (const r of runs) out(agentOut.runLine(r)); return 0; }
if (o.json) { out(JSON.stringify(runs, null, 2)); return 0; }
if (o.json) { out(JSON.stringify(runs, null, 2)); return 0; }
for (const r of runs) {
for (const r of runs) {
const ok = r.result && !r.result.is_error;
const ok = r.result && !r.result.is_error;
@@ -203,15 +241,25 @@ async function main() {
@@ -203,15 +241,25 @@ async function main() {
}
}
if (cmd === 'show') {
if (cmd === 'show') {
const r = harness.store.loadRun(o._[0]);
const r = harness.store.loadRun(o._[0]);
- if (!r) { out(`no run ${o._[0]} on this machine`); return 1; }
+ if (!r) {
+ if (o.forAgent) out(agentOut.errorLine('no_run', `no run ${o._[0]} on this machine`));
+ else out(`no run ${o._[0]} on this machine`);
+ return 1;
+ }
+ if (o.forAgent) { for (const l of agentOut.showLines(r)) out(l); return r.result && !r.result.is_error ? 0 : 1; }
if (o.json) { for (const e of harness.store.events(r.run_id)) out(JSON.stringify(e)); return 0; }
if (o.json) { for (const e of harness.store.events(r.run_id)) out(JSON.stringify(e)); return 0; }
out(`${r.run_id} · ${r.cwd} · ${r.permission}`);
out(`${r.run_id} · ${r.cwd} · ${r.permission}`);
for (const a of r.attempts || []) out(` ${a.ok ? '✔' : '✗'} ${a.agent}${a.session_id ? dim(` session ${a.session_id}`) : ''}${a.error ? ` — ${a.error}` : ''}`);
for (const a of r.attempts || []) out(` ${a.ok ? '✔' : '✗'} ${a.agent}${a.session_id ? dim(` session ${a.session_id}`) : ''}${a.error ? ` — ${a.error}` : ''}`);
if (r.result) out(r.result.result || '');
if (r.result) out(r.result.result || '');
return r.result && !r.result.is_error ? 0 : 1;
return r.result && !r.result.is_error ? 0 : 1;
}
}
+ if (o.forAgent) { out(agentOut.errorLine('usage', `unknown command or option: ${[cmd, ...rest].filter(Boolean).join(' ')}`)); return 1; }
out(USAGE);
out(USAGE);
return 1;
return 1;
}
}
-main().then((code) => process.exit(code), (err) => { process.stderr.write(`openvibe-codes: ${err.message}\n`); process.exit(1); });
+main().then((code) => process.exit(code), (err) => {
+ if (agentMode) process.stdout.write(`${agentOut.errorLine('internal', err && err.message)}\n`);
+ process.stderr.write(`openvibe-codes: ${err && err.message}\n`);
+ process.exit(1);
+});
new file mode 100644
new file mode 100644
@@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
+'use strict';
+
+/**
+ * `openvibe-codes skills`: the guide an agent reads instead of a human pasting the CLI reference into a prompt.
+ *
+ * The guide is a file shipped inside the package, so it is version-matched by construction: `{{version}}` in the file
+ * is replaced with this binary's own version, and a test fails if the guide and the CLI's flags drift apart. An agent
+ * loads it on demand:
+ *
+ * openvibe-codes skills list # the guides this binary ships
+ * openvibe-codes skills get # the openvibe-codes guide (Markdown, on stdout)
+ * openvibe-codes skills get openvibe-codes # the same, named
+ * openvibe-codes skills path # where the file is, for a host that loads files itself
+ */
+
+const fs = require('fs');
+const path = require('path');
+const { version: VERSION } = require('../package.json');
+
+const DIR = path.join(__dirname, 'skills');
+
+// One guide today. Aliases keep `skills get codes` working; a second guide is one entry here and one file.
+const GUIDES = [
+ { name: 'openvibe-codes', aliases: ['codes'], file: 'openvibe-codes.md', description: 'run coding agents from a script or another agent: commands, --for-agent lines, exit codes, error codes, resume' },
+];
+
+const byName = (name) => GUIDES.find((g) => g.name === name || (g.aliases || []).includes(name)) || null;
+
+/** Every guide this binary ships: its name, version, description and file. */
+function list() {
+ return GUIDES.map((g) => ({ name: g.name, version: VERSION, description: g.description, path: path.join(DIR, g.file) }));
+}
+
+/** One guide's Markdown, with the version this binary reports put in (it always matches the CLI it came from). */
+function read(name = 'openvibe-codes') {
+ const g = byName(name);
+ if (!g) return null;
+ const file = path.join(DIR, g.file);
+ let text;
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return null; }
+ return text.replace(/\{\{version\}\}/g, VERSION);
+}
+
+/**
+ * The `skills` command. `out` is stdout's writer; the return value is the exit code.
+ *
+ * @param {string[]} argv words after `skills`
+ * @param {{out: (s: string) => void, json?: boolean}} io
+ */
+function skills(argv, { out, json = false } = {}) {
+ const [sub = 'list', name] = argv;
+ if (sub === 'list') {
+ const rows = list();
+ if (json) { out(JSON.stringify(rows)); return 0; }
+ for (const r of rows) out(`${r.name} ${r.version} ${r.description}`);
+ return 0;
+ }
+ if (sub === 'get') {
+ const text = read(name || 'openvibe-codes');
+ if (text == null) {
+ out(`no guide named ${name || ''} (openvibe-codes skills list)`);
+ return 1;
+ }
+ out(text.trimEnd());
+ return 0;
+ }
+ if (sub === 'path') {
+ const g = byName(name || 'openvibe-codes');
+ if (!g) { out(`no guide named ${name || ''} (openvibe-codes skills list)`); return 1; }
+ out(path.join(DIR, g.file));
+ return 0;
+ }
+ out(`unknown skills command ${sub} (openvibe-codes skills list|get|path [name])`);
+ return 1;
+}
+
+module.exports = { GUIDES, list, read, skills, byName };
new file mode 100644
new file mode 100644
@@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
+# openvibe-codes — the guide for agents
+
+Version: {{version}} (shipped inside the binary; it always matches `openvibe-codes --version`)
+
+`openvibe-codes` runs a coding task on an agent CLI installed on **this machine** — Claude Code, Codex, OpenCode,
+Command Code, MiniMax Code or Aider — or on the DeepSeek API or any OpenAI-compatible server. The harness routes the
+task, hands it to the next agent when one fails, and keeps every run. Nothing is sent to OpenVibe.
+
+Use this guide when you are an agent driving `openvibe-codes` (or a script that supervises one). Read it with
+`openvibe-codes skills get`; do not paste the human documentation into a prompt.
+
+## First, what is here
+
+```bash
+openvibe-codes agents --for-agent # one line: which harness ids this machine can run, and why not the rest
+openvibe-codes logins --for-agent # installed, signed in, each provider's limit windows
+openvibe-codes route --for-agent --task edit # which agent a task would go to, and why
+```
+
+Only run an id that `agents --for-agent` reports `"available":true`. An agent that is not installed, not signed in
+or at its usage limit will not be picked.
+
+## Run a task
+
+```bash
+openvibe-codes run --for-agent "rename getUser to findUser"
+openvibe-codes run --for-agent --agent codex --permission read "where is the session cookie set?"
+openvibe-codes run --for-agent --task review "review the diff on this branch"
+openvibe-codes run --for-agent - < brief.md # the task on stdin
+```
+
+- `--agent <id>` skips routing and runs that harness first. Without it the harness picks, cheapest suitable first,
+ and hands over on failure.
+- `--permission read|edit|full` — `read` looks only, `edit` (default) changes files, `full` is anything unattended.
+ OpenCode and Command Code have no edits-only mode; outside a sandbox they need `full`.
+- `--model`, `--effort low|medium|high|max`, `--advisor` are passed to the first agent.
+- `--cwd <dir>` is where the agent works (default: the current directory).
+- `--no-handoff` stops at the first agent's result; `--attempts <n>` caps the agents in one run (default 3).
+- `--json` is the full event stream for a person debugging; **use `--for-agent` instead**.
+
+## The `--for-agent` envelope
+
+One compact JSON object per line on stdout, no ANSI and no pretty printing. Every line has fixed fields:
+
+| field | meaning |
+|---|---|
+| `run` | the run id; `show` and `resume` take it |
+| `seq` | the line's number in this run, from 1 |
+| `event` | `init`, `text`, `tool`, `tool_error`, `progress`, `handoff`, `warning`, `stderr`, `result`; `show`/`runs` also answer `run`, `attempt`, `error` |
+| `agent` | the harness that produced the line |
+| `session` | the harness's session id, when it has one (what `resume` continues) |
+| `ref` | what a tool call points at: a file, a pattern, a URL or the first line of a command |
+| `ok` | on a result: did the agent finish without an error |
+| `error` | on a failure: `{ code, retry, suggestion }` |
+| `text` | the message or answer, bounded to 400 characters |
+
+A successful run ends with exactly one `result` line with `"ok":true`. A failed run ends with one `result` line
+carrying `"ok":false` and a `code`; a failure before any agent ran is one `error` line. Text is truncated to keep an
+agent's context small; the human log (no `--for-agent`) has all of it.
+
+Example lines:
+
+```json
+{"run":"run_01J…","seq":1,"event":"init","agent":"codex","model":"gpt-6","session":"cx-1"}
+{"run":"run_01J…","seq":2,"event":"tool","agent":"codex","tool":"Edit","ref":"src/user.js"}
+{"run":"run_01J…","seq":3,"event":"result","agent":"codex","ok":true,"session":"cx-1","text":"renamed getUser to findUser","cost_usd":0.02}
+```
+
+## Reading a run without re-running it
+
+```bash
+openvibe-codes runs --for-agent # one line per kept run on this machine
+openvibe-codes show <run> --for-agent # the header, each attempt, the result (not every event)
+openvibe-codes resume <run> --for-agent "now add the test" # continue the last session with the same agent
+```
+
+`show --json` dumps every event; an agent should use `show --for-agent`, which is bounded.
+
+## Errors and retries
+
+Failures carry `{code, retry, suggestion}`. Branch on `code`, never on the message text.
+
+| code | retry | what to do |
+|---|---|---|
+| `no_prompt` | false | give the task as an argument, or `-` plus stdin |
+| `bad_agent` | false | the id is not in the catalog: read `agents --for-agent` |
+| `unavailable` | false | the harness is not installed or signed in here: read `agents --for-agent` |
+| `agent_failed` | false | read the result, then `resume` the same session with what is missing |
+| `no_run` | false | the id is not on this machine: read `runs --for-agent` |
+| `usage` | false | a wrong command line: read `skills get` |
+| `internal` | false | an unexpected error: run the command without `--for-agent` for the full message |
+
+`retry` is `true` only when repeating the exact same command is safe. An agent run is not idempotent (it edits the
+working tree), so a failed run is continued with `resume`, not started again.
+
+## Exit codes
+
+| code | meaning |
+|---|---|
+| 0 | the last agent finished without an error |
+| 1 | a failure: the task failed, no agent could take it, or the command line was wrong |
+| 2 | `task` only: the command line or the Network token is wrong (nothing was sent) |
+
+## Tasks on openvibe.codes
+
+`openvibe-codes task` hands a task to openvibe.codes/run and one of the person's machines runs it there:
+
+```bash
+openvibe-codes task start --repo OpenVibers/OpenVibe.Codes --file brief.md --name codes-c6 --tier hard
+openvibe-codes task list --state running|needs|done
+openvibe-codes task show <task> --follow
+openvibe-codes task continue <task> --note "and add the test the reviewer asked for"
+openvibe-codes task stop <task>
+```
+
+It needs a personal Network token (`OPENVIBE_TOKEN`, or `~/.config/openvibe/token`). `docs/cli.md` in the repository
+has its flags and the task API it maps onto.
+
+## The rules of this machine
+
+- Do not parse the human log: use `--for-agent`.
+- Do not assume an agent's tool names: `tool` and `ref` are normalised to Claude Code's vocabulary.
+- Do not repeat a mutating run; `resume` it with a follow-up.
+- The harness never prints a token, and neither should you: `openvibe-codes` scrubs secrets out of events.
+- The run is kept in `OPENVIBE_CODES_HOME` (else `$XDG_STATE_HOME/openvibe-codes`, else `~/.local/state/openvibe-codes`).
@@ -52,6 +52,33 @@ openvibe-codes task stop tsk_01J…
@@ -52,6 +52,33 @@ openvibe-codes task stop tsk_01J…
`GET /api/v1/tasks/:id/events` and prints the session's lines, each attempt and why it happened, ending when the
`GET /api/v1/tasks/:id/events` and prints the session's lines, each attempt and why it happened, ending when the
outcome is decided.
outcome is decided.
+## For an agent: `--for-agent` and `skills get`
+
+A supervising agent should not read the human log. `--for-agent` is the machine face of every command in this
+document that runs or reads a run: one compact JSON object per line, no ANSI and no pretty printing, with fixed
+fields — `run`, `seq`, `event`, `agent`, `session`, `ref`, `ok`, and on a failure `error: {code, retry,
+suggestion}`. Text is bounded to 400 characters; `--json` stays the full event stream for a person debugging.
+
+```bash
+openvibe-codes agents --for-agent # one line per harness, with why it cannot run
+openvibe-codes run --for-agent "rename getUser to findUser"
+openvibe-codes runs --for-agent
+openvibe-codes show <run id> --for-agent # the header, each attempt, the result (not every event)
+openvibe-codes resume <run id> --for-agent "now add the test"
+```
+
+`retry` is true only when repeating the exact command is safe; an agent run is not idempotent, so a failed run is
+continued with `resume`. The closed set of error codes is in the guide the binary ships:
+
+```bash
+openvibe-codes skills list # the guides inside this binary
+openvibe-codes skills get # the openvibe-codes guide, Markdown, for the exact version you are running
+openvibe-codes skills path # where the file is, for a host that loads files itself
+```
+
+The guide is version-matched by construction: it is in the package and its `{{version}}` line is replaced with the
+binary's own version, and a test keeps it and the CLI's flags in step.
+
## Exit codes
## Exit codes
| code | meaning |
| code | meaning |
new file mode 100644
new file mode 100644
@@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
+'use strict';
+/**
+ * The agent face of `openvibe-codes`: `--for-agent` (one compact JSON line per event, fixed fields, typed errors) and
+ * `skills get` (the version-matched guide shipped inside the binary). The human log and --json stay as they were.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+const { version } = require('../package.json');
+const { fakeAgents } = require('./helpers/fake-agents');
+const { check, done } = require('./helpers/boot');
+
+const BIN = path.join(__dirname, '..', 'bin', 'openvibe-codes.js');
+
+(async () => {
+ const f = fakeAgents({ codex: 'ok' });
+ const cli = (...args) => {
+ const r = spawnSync(process.execPath, [BIN, ...args], { cwd: f.work, env: { ...process.env, ...f.env }, encoding: 'utf8', timeout: 60_000 });
+ return { code: r.status, out: r.stdout, err: r.stderr };
+ };
+ const lines = (out) => out.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
+
+ await check('skills get: the guide is shipped with this binary, at its version', () => {
+ const r = cli('skills', 'get');
+ assert.strictEqual(r.code, 0, r.err);
+ assert.match(r.out, new RegExp(`Version: ${version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
+ assert.match(r.out, /--for-agent/);
+ assert.match(r.out, /skills get/);
+ assert.ok(!r.out.includes('{{version}}'), 'the version placeholder is replaced');
+ const list = JSON.parse(cli('skills', 'list', '--json').out);
+ assert.deepStrictEqual(list.map((g) => g.name), ['openvibe-codes']);
+ assert.strictEqual(list[0].version, version);
+ const where = cli('skills', 'path').out.trim();
+ assert.ok(fs.existsSync(where), where);
+ assert.strictEqual(cli('skills', 'get', 'nope').code, 1);
+ });
+
+ await check('run --for-agent: one compact JSON line per event, a fixed envelope and a bounded result', () => {
+ const r = cli('run', '--agent', 'codex', '--no-handoff', '--for-agent', 'x'.repeat(1200));
+ assert.strictEqual(r.code, 0, r.out + r.err);
+ const rows = lines(r.out);
+ assert.ok(rows.length >= 3, r.out);
+ assert.strictEqual(rows[0].event, 'init');
+ assert.strictEqual(rows[0].agent, 'codex');
+ assert.ok(rows[0].run, 'the run id is on every line');
+ assert.deepStrictEqual(rows.map((x) => x.seq), rows.map((_, i) => i + 1), 'seq counts the lines');
+ assert.ok(rows.every((x) => x.run === rows[0].run));
+ assert.ok(rows.some((x) => x.event === 'tool' && x.ref === 'a.js'), r.out);
+ const result = rows[rows.length - 1];
+ assert.strictEqual(result.event, 'result');
+ assert.strictEqual(result.ok, true);
+ assert.ok(result.text.length <= 400, 'the answer is bounded');
+ assert.ok(!/\x1b\[/.test(r.out), 'no ANSI in the agent output');
+ assert.ok(!r.out.includes('▶'), 'not the human log');
+ });
+
+ await check('a failed run ends with {code, retry, suggestion}; a missing agent is unavailable', () => {
+ f.env.FAKE_CODEX = 'fail';
+ const r = cli('run', '--agent', 'codex', '--no-handoff', '--for-agent', 'x');
+ assert.strictEqual(r.code, 1);
+ const result = lines(r.out).pop();
+ assert.strictEqual(result.event, 'result');
+ assert.strictEqual(result.ok, false);
+ assert.strictEqual(result.code, 'agent_failed');
+ assert.strictEqual(result.retry, false);
+ assert.match(result.suggestion, /resume/);
+ f.env.FAKE_CODEX = 'ok';
+
+ const missing = cli('run', '--agent', 'not-an-agent', '--for-agent', 'x');
+ assert.strictEqual(missing.code, 1);
+ const refused = lines(missing.out).pop();
+ assert.strictEqual(refused.code, 'unavailable');
+ assert.strictEqual(refused.retry, false);
+ assert.match(refused.suggestion, /agents --for-agent/);
+
+ const none = cli('run', '--for-agent');
+ assert.strictEqual(none.code, 1);
+ assert.strictEqual(lines(none.out)[0].code, 'no_prompt');
+ });
+
+ await check('show, runs, agents and route answer the same envelope', () => {
+ const run = cli('run', '--agent', 'codex', '--no-handoff', '--for-agent', 'hello');
+ const runId = lines(run.out)[0].run;
+ const show = cli('show', runId, '--for-agent');
+ assert.strictEqual(show.code, 0, show.out + show.err);
+ const rows = lines(show.out);
+ assert.deepStrictEqual(rows.map((x) => x.event), ['run', 'attempt', 'result']);
+ assert.strictEqual(rows[0].run, runId);
+ assert.strictEqual(rows[2].ok, true);
+ assert.strictEqual(cli('show', 'run_nope', '--for-agent').code, 1);
+
+ const runs = lines(cli('runs', '--for-agent').out);
+ assert.ok(runs.some((x) => x.run === runId && x.event === 'run'));
+
+ const agents = lines(cli('agents', '--for-agent').out);
+ assert.ok(agents.find((a) => a.id === 'codex' && a.available === true));
+ assert.ok(agents.find((a) => a.id === 'aider' && a.available === false && a.reason));
+
+ const route = lines(cli('route', '--task', 'review', '--for-agent').out);
+ assert.strictEqual(route.length, 1);
+ assert.strictEqual(route[0].event, 'route');
+ assert.ok(route[0].selected);
+ });
+
+ f.cleanup();
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });