openvibers/openvibe.codes Commits

The CLI an agent can drive: --for-agent and a shipped guide (#368)

OP OpenVibers committed 3 days ago · 21f99f8d98 · 6 files, +530 −7

Changes

ddiff --git a/bin/agent-output.js b/bin/agent-output.js
nnew file mode 100644
iindex 0000000..ff64a60
---- /dev/null
++++ b/bin/agent-output.js
@@@ -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 };
ddiff --git a/bin/openvibe-codes.js b/bin/openvibe-codes.js
iindex d4fdf70..3dfb13d 100755
---- a/bin/openvibe-codes.js
++++ b/bin/openvibe-codes.js
@@@ -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 binary
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 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 machine
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
@@@ -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);
++});
ddiff --git a/bin/skills.js b/bin/skills.js
nnew file mode 100644
iindex 0000000..9d90dfc
---- /dev/null
++++ b/bin/skills.js
@@@ -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 };
ddiff --git a/bin/skills/openvibe-codes.md b/bin/skills/openvibe-codes.md
nnew file mode 100644
iindex 0000000..7e23e95
---- /dev/null
++++ b/bin/skills/openvibe-codes.md
@@@ -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`).
ddiff --git a/docs/cli.md b/docs/cli.md
iindex fd089f2..ae58fff 100644
---- a/docs/cli.md
++++ b/docs/cli.md
@@@ -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
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
| code | meaning |
ddiff --git a/test/agent-cli.test.js b/test/agent-cli.test.js
nnew file mode 100644
iindex 0000000..b5667e8
---- /dev/null
++++ b/test/agent-cli.test.js
@@@ -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); });