openvibers/openvibe.codes Commits
Per-turn checkpoint refs and a turn diff (#372)
Changes
nnew file mode 100644@@@ -0,0 +1,55 @@++# Per-turn checkpoints++++A coding job clones the repository into a scratch directory and an agent works there. `harness/checkpoints.js` pins++that worktree to a git commit before each tool batch and keeps it under a ref:++++```++refs/codes/checkpoints/<task>/<run>/<n>++```++++The refs are the run's own history. They are never pushed and never become a branch, and the clone (and so the refs)++is removed when the job ends. What survives is the job result: the refs, each turn's diff, and the final turn diff++against the clone as the agent found it.++++## How a checkpoint is written++++A checkpoint uses a temporary index (`GIT_INDEX_FILE`) so it never touches the run's own index:++++```++read-tree HEAD → add -A → write-tree → commit-tree -p <previous checkpoint> → update-ref++```++++`.gitignore` and `.git/info/exclude` still apply, so `node_modules`, build output and the owner's `.openvibe/` files++stay out. `n = 0` is the clone as the agent found it; the job takes one more at the end (`final`) for the last turn.++++## The turn diff++++`checkpoints.turns[]` has one row per checkpoint after the start:++++```json++{++ "n": 2, "ref": "refs/codes/checkpoints/tsk_…/run_…/2", "label": "turn 1",++ "from": 1, "to": 2, "files_total": 3, "authored": 2, "imported": 1, "truncated": false,++ "files": [++ { "path": "src/user.js", "status": "M", "added": 4, "removed": 3, "authored": true },++ { "path": "package-lock.json", "status": "M", "added": 120, "removed": 2, "authored": false }++ ]++}++```++++A file is **authored** when a tool call named it during the run (Claude Code's tool vocabulary, whatever harness ran)++and **imported** otherwise: a patch applied from an earlier conversation, a dependency install, a formatter, a++subagent's work. `status` is git's letter (`A`, `M`, `D`, `R`); `checkpoints.turn_diff` is the whole run against++`n = 0`. A result carries at most 20 turns and 20 files a turn (`files_total` says how many there really were).++++## Restoring++++`createCheckpoints(...).restore(n)` puts the worktree back to a checkpoint: `read-tree --reset -u` of its tree, then++`git clean -fdq` for the untracked files a broken turn left (ignored files stay). The last checkpoint is the last good++state before the current turn. This is the operation a **failed repair** uses: the repair loop (After-Loops AL2, the++same builder resumed with findings) restores the last good turn before it retries, rather than asking the model to++re-read and re-edit from a broken tree. Routine hand-offs do **not** restore: when an agent runs out of its plan++mid-work, the next agent keeps that work (the hand-off note tells it what was done).++++The module is tested in `test/checkpoints.test.js`; a job's integration (refs written, the agent's own file marked++authored, the failed run still carrying its diffs) runs in the same file against a real local clone.@@@ -35,19 +35,20 @@ nothing, and is excluded only when a requirement is set.| harness | terminal_status | steering | checkpointing | subagents | context_handoff | enforcement ||---|---|---|---|---|---|---|--| `claude-code` | structured | resume | none | native | session | native |--| `claude-other` | structured | resume | none | native | session | native |--| `codex` | structured | resume | none | none | session | native |--| `opencode` | structured | resume | none | none | session | client-boundary |--| `command-code` | structured | resume | none | none | session | client-boundary |--| `aider` | parsed | none | none | none | none | client-boundary |--| `minimax-code` | structured | resume | none | none | session | native |--| `deepseek` | structured | resume | none | none | session | client-boundary |--| `openvibe-ai` | structured | resume | none | none | session | client-boundary |--| `openai-compatible` | structured | resume | none | none | session | client-boundary |++| `claude-code` | structured | resume | git | native | session | native |++| `claude-other` | structured | resume | git | native | session | native |++| `codex` | structured | resume | git | none | session | native |++| `opencode` | structured | resume | git | none | session | client-boundary |++| `command-code` | structured | resume | git | none | session | client-boundary |++| `aider` | parsed | none | git | none | none | client-boundary |++| `minimax-code` | structured | resume | git | none | session | native |++| `deepseek` | structured | resume | git | none | session | client-boundary |++| `openvibe-ai` | structured | resume | git | none | session | client-boundary |++| `openai-compatible` | structured | resume | git | none | session | client-boundary |--`checkpointing` flips to `git` on every row when the per-turn checkpoint refs land (`harness/job.js`): the manifest is--the runtime's declaration, so it must not claim a checkpoint before one exists.++`checkpointing` is `git` on every row: a job pins its clone to `refs/codes/checkpoints/<task>/<run>/<n>` per turn++(`harness/job.js`, `harness/checkpoints.js`) and can restore the last good one; the refs are the run's own and are++never pushed.## Using it@@@ -65,48 +65,49 @@ const CAPABILITY = Object.freeze({** Grounded in each adapter (harness/adapters/cli.js, harness/adapters/api.js) and its CLI's flags, not in its name:* every CLI here ends with a result line except Aider (its prose is parsed); every CLI with `--resume`/`--session`/`-s`-- * continues its session; Claude Code alone exposes subagents as tools. `checkpointing` is `none` until the per-turn-- * checkpoint refs land: this manifest is the runtime's declaration, so it must not claim one early.++ * continues its session; Claude Code alone exposes subagents as tools. `checkpointing` is `git` for every harness:++ * the job pins its clone to `refs/codes/checkpoints/<task>/<run>/<n>` per turn (harness/checkpoints.js), whatever the++ * harness.*/const MANIFEST = Object.freeze({'claude-code': Object.freeze({-- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',++ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',subagents: 'native', context_handoff: 'session', enforcement: 'native',}),'claude-other': Object.freeze({-- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',++ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',subagents: 'native', context_handoff: 'session', enforcement: 'native',}),codex: Object.freeze({-- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',++ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',subagents: 'none', context_handoff: 'session', enforcement: 'native',}),opencode: Object.freeze({-- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',++ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',}),'command-code': Object.freeze({-- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',++ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',}),aider: Object.freeze({-- terminal_status: 'parsed', steering: 'none', checkpointing: 'none',++ terminal_status: 'parsed', steering: 'none', checkpointing: 'git',subagents: 'none', context_handoff: 'none', enforcement: 'client-boundary',}),'minimax-code': Object.freeze({-- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',++ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',subagents: 'none', context_handoff: 'session', enforcement: 'native',}),deepseek: Object.freeze({-- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',++ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',}),'openvibe-ai': Object.freeze({-- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',++ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',}),'openai-compatible': Object.freeze({-- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',++ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',}),});nnew file mode 100644@@@ -0,0 +1,209 @@++'use strict';++++/**++ * Per-turn checkpoints in a job's clone: before each model turn (really before each tool batch, which is the closest++ * boundary the stream shows) the working tree is written to a git commit and pinned under++ * `refs/codes/checkpoints/<task>/<run>/<n>`. Nothing is committed to a branch and nothing is pushed: the refs are the++ * run's own history, and the job's clone is thrown away after it.++ *++ * const cp = createCheckpoints({ cwd, task, run });++ * cp.capture('start'); // n = 0, the clone as the agent found it++ * cp.authored(['src/user.js']); // paths this turn's tools wrote++ * cp.capture('turn'); // n = 1, after the turn++ * cp.diff(0, 1); // { files: [{ path, status, added, removed, authored }], … }++ * cp.restore(0); // put the worktree back to the last good turn (the repair path)++ *++ * The diff marks a path **authored** when a tool call named it in the turn and **imported** otherwise (a patch applied,++ * a dependency install, a formatter or a subagent): the first is the agent's work, the second is something that came++ * from elsewhere, and a review should be told them apart.++ *++ * A checkpoint is built with a temporary index (`GIT_INDEX_FILE`) so it never touches the run's own index:++ * `read-tree HEAD` → `add -A` → `write-tree` → `commit-tree` with the previous checkpoint as parent → `update-ref`.++ * `.gitignore` and `.git/info/exclude` still apply, so build output, node_modules and the owner's `.openvibe/` files++ * stay out.++ */++++const fs = require('fs');++const os = require('os');++const path = require('path');++const { execFileSync } = require('child_process');++++/** A cap on a run's refs and on a diff's rows, so a very long turn cannot flood the result. */++const MAX_TURNS = 200;++const MAX_FILES = 100;++++/** git in the worktree, no prompt and no credential helper; a failure names the command and the last line. */++function git(cwd, args, env = {}) {++ try {++ return execFileSync('git', args, {++ cwd, encoding: 'utf8', timeout: 30_000, maxBuffer: 16 * 1024 * 1024,++ env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo', ...env },++ stdio: ['ignore', 'pipe', 'pipe'],++ });++ } catch (error) {++ const said = String(error.stderr || error.message || '').trim().split('\n').slice(-2).join(' ');++ const err = new Error(`checkpoint: git ${args[0]} failed: ${said}`);++ err.code = 'checkpoint.git';++ throw err;++ }++}++++/** A ref path component that git accepts and that a person can read back (a task id is already safe). */++function safeName(value) {++ return String(value == null ? '' : value).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80) || 'run';++}++++/** The file paths a harness event wrote, from its tool calls (Claude Code's names are the shared vocabulary). */++function toolPaths(event) {++ const out = [];++ if (!event || event.type !== 'assistant') return out;++ for (const c of (event.message && event.message.content) || []) {++ if (!c || c.type !== 'tool_use') continue;++ const i = c.input && typeof c.input === 'object' ? c.input : {};++ for (const key of ['file_path', 'path', 'notebook_path']) if (typeof i[key] === 'string' && i[key]) out.push(i[key]);++ // A patch helper names its files in a list; an edit may carry a list of edits for one path.++ if (Array.isArray(i.files)) out.push(...i.files.filter((f) => typeof f === 'string'));++ }++ return out;++}++++/**++ * The checkpoints of one run.++ *++ * @param {object} opts++ * @param {string} opts.cwd the clone++ * @param {string} [opts.task] the task id (the ref's first component)++ * @param {string} [opts.run] the run id (the ref's second component)++ * @param {number} [opts.maxTurns] the most refs one run writes++ * @param {number} [opts.maxFiles] the most rows one diff carries++ * @param {() => number} [opts.now]++ */++function createCheckpoints({ cwd, task = 'task', run = 'run', maxTurns = MAX_TURNS, maxFiles = MAX_FILES, now = Date.now } = {}) {++ const root = `refs/codes/checkpoints/${safeName(task)}/${safeName(run)}`;++ const entries = [];++ const pending = new Set();++ // Every path a tool has named in this run: a diff marks a file authored if the agent touched it, however many++ // turns the write and the event that reports it fall apart by (a CLI prints the call before or after it runs).++ const named = new Set();++++ const head = () => git(cwd, ['rev-parse', 'HEAD']).trim();++++ /**++ * Capture the worktree now as checkpoint `n`. The parent is the previous checkpoint (or HEAD for the first), so the++ * chain is inspectable with `git log`. The paths tools wrote since the previous capture are this checkpoint's++ * authored set.++ *++ * @param {string} [label] what the capture marks (start, turn, final)++ * @returns {object|null} the entry, or null once the cap is reached++ */++ function capture(label = 'turn') {++ if (entries.length >= maxTurns) return null;++ const n = entries.length;++ const index = path.join(os.tmpdir(), `codes-index-${process.pid}-${Date.now()}-${n}`);++ const env = { GIT_INDEX_FILE: index };++ try {++ git(cwd, ['read-tree', 'HEAD'], env);++ git(cwd, ['add', '-A'], env);++ const tree = git(cwd, ['write-tree'], env).trim();++ const parent = entries.length ? entries[entries.length - 1].sha : head();++ const sha = git(cwd, [++ '-c', 'user.name=OpenVibe.Codes', '-c', '[email protected]',++ 'commit-tree', tree, '-p', parent, '-m', `codes checkpoint ${n} ${label}`,++ ]).trim();++ const ref = `${root}/${n}`;++ git(cwd, ['update-ref', ref, sha]);++ const entry = { n, ref, sha, tree, label, at: new Date(now()).toISOString(), authored: [...pending] };++ pending.clear();++ entries.push(entry);++ return entry;++ } finally {++ try { fs.rmSync(index, { force: true }); } catch { /* the temp index is best effort */ }++ }++ }++++ /** The paths the current turn's tools wrote: a per-turn snapshot, and the run's cumulative authored set. */++ function authored(paths) {++ for (const p of paths || []) if (p) { pending.add(String(p)); named.add(String(p)); }++ }++++ /** Resolve a checkpoint by number, ref or sha. */++ function entryOf(which) {++ if (which && typeof which === 'object') return which;++ return entries.find((e) => e.n === Number(which) || e.ref === which || e.sha === which) || null;++ }++++ /**++ * The diff one turn made, with its paths split by provenance.++ *++ * @param {number|string|object} from checkpoint n / ref / sha++ * @param {number|string|object} to checkpoint n / ref / sha++ * @returns {{from: number|null, to: number|null, files: object[], files_total: number, authored: number, imported: number}}++ */++ function diff(from, to) {++ const a = entryOf(from);++ const b = entryOf(to);++ const empty = { from: a ? a.n : null, to: b ? b.n : null, files: [], files_total: 0, authored: 0, imported: 0 };++ if (!a || !b) return empty;++ const status = git(cwd, ['diff', '--name-status', '--find-renames', a.sha, b.sha]).trim();++ if (!status) return empty;++ const stats = new Map();++ for (const row of git(cwd, ['diff', '--numstat', a.sha, b.sha]).trim().split('\n').filter(Boolean)) {++ const [added, removed, p] = row.split('\t');++ if (p == null) continue;++ // A rename's numstat path is `old => new` (or `{old => new}` for a shared prefix): keep the newest path.++ const file = p.includes(' => ') ? p.replace(/^.* => /, '').replace(/[{}]/g, '').trim() : p;++ stats.set(file, { added: Number(added) || 0, removed: Number(removed) || 0 });++ }++ const authoredSet = named;++ const files = [];++ for (const line of status.split('\n')) {++ const parts = line.split('\t');++ const code = String(parts[0] || '').charAt(0);++ const file = code === 'R' || code === 'C' ? parts[2] : parts[1];++ if (!file) continue;++ const nums = stats.get(file) || { added: 0, removed: 0 };++ files.push({ path: file, status: code, added: nums.added, removed: nums.removed, authored: authoredSet.has(file) || (code === 'R' && authoredSet.has(parts[1])) });++ }++ const authored = files.filter((f) => f.authored).length;++ return { from: a.n, to: b.n, files: files.slice(0, maxFiles), files_total: files.length, authored, imported: files.length - authored };++ }++++ /**++ * Put the worktree back to a checkpoint: index and tracked files from its tree, then the untracked files a broken++ * turn left are cleaned (ignored files, including `.openvibe/`, stay).++ *++ * @param {number|string|object} which checkpoint n / ref / sha++ * @returns {{n: number, ref: string, sha: string}|null}++ */++ function restore(which) {++ const entry = entryOf(which);++ if (!entry) return null;++ git(cwd, ['read-tree', '--reset', '-u', entry.sha]);++ try { git(cwd, ['clean', '-fdq']); } catch { /* a clean failure leaves the restored tree as is */ }++ return { n: entry.n, ref: entry.ref, sha: entry.sha };++ }++++ /** The refs written, oldest first, without the paths (a result carries the diffs separately). */++ function list() {++ return entries.map((e) => ({ n: e.n, ref: e.ref, sha: e.sha, label: e.label, at: e.at }));++ }++++ /** The last checkpoint (the last good state before the current turn). */++ function last() {++ return entries[entries.length - 1] || null;++ }++++ /** Every turn's diff from the previous checkpoint, one row per ref after the first. */++ function turns() {++ const out = [];++ for (let i = 1; i < entries.length; i++) {++ const d = diff(entries[i - 1], entries[i]);++ out.push({ n: entries[i].n, ref: entries[i].ref, sha: entries[i].sha, label: entries[i].label, at: entries[i].at, ...d });++ }++ return out;++ }++++ return { root, capture, authored, diff, restore, list, last, turns, entries, safeName };++}++++module.exports = { createCheckpoints, toolPaths, safeName, MAX_TURNS, MAX_FILES };@@@ -9,15 +9,17 @@** It clones `repo` (https only) into a scratch directory, runs the harness there with hand-offs, and prints one compact* JSON line per event on stdout (what the page shows live). The result goes to fd 3 (the Link's result channel), or as-- * a last `{ t: 'result_doc' }` line when fd 3 is not open: what the agents said, the changed files and the patch, and,-- * when `push` is asked and the machine holds GITHUB_TOKEN, the new branch it pushed (never the base branch) and its-- * pull request. Nothing here ever prints a token.++ * a last `{ t: 'result_doc' }` line when fd 3 is not open: what the agents said, the changed files and the patch, the++ * per-turn checkpoints (refs and the authored/imported turn diff, `checkpoints` in the document), and, when `push` is++ * asked and the machine holds GITHUB_TOKEN, the new branch it pushed (never the base branch) and its pull request.++ * Nothing here ever prints a token.*/const fs = require('fs');const os = require('os');const path = require('path');const { execFile } = require('child_process');const { collectProof } = require('../engine/proof');++const { createCheckpoints, toolPaths } = require('./checkpoints');const JOB_PROTOCOL = '1.0.0';/** The agents a conductor's turn may run on: every harness Codes offers. */@@@ -248,6 +250,8 @@ function parseArgs(raw, env = process.env) {} : null;return {kind, repo: repo ? repo.toString() : null, ref, prompt, permission, push,++ // The task this job is an attempt of, when the server names it: the checkpoint refs carry it.++ ...(typeof a.task_id === 'string' && /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/.test(a.task_id) ? { taskId: a.task_id } : {}),...(attached.length ? { attachments: attached } : {}),...(limits ? { limits } : {}),...(resume ? { resume } : {}),@@@ -782,11 +786,34 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tmresume: job.resume || null,...(adapterOptions ? { adapterOptions } : {}),});++ // Per-turn checkpoints (Release 1, feat 11): the clone as the agent found it, then one ref per tool batch. They++ // live in this clone only and are never pushed; the result carries the refs and the turn diffs so a review can++ // see the agent's own work apart from what came from elsewhere. A checkpoint failure never fails the run.++ let checkpoints = null;++ if (job.kind === 'code') {++ try {++ checkpoints = createCheckpoints({ cwd: work, task: job.taskId || 'task', run: env.OPENVIBE_JOB_ID || run.runId });++ checkpoints.capture('start');++ } catch (e) {++ say({ t: 'note', level: 'warning', text: `Checkpoints are off for this run: ${scrub(e.message)}` });++ checkpoints = null;++ }++ }let last = null;// What the pictures may take of the result's room.const pictures = { left: MAX_RESULT_BYTES };for await (const e of run) {last = e;++ // Before the tool batch the model just called runs, pin the tree: this is the checkpoint a failed repair++ // returns to. The paths the batch names become the authored set of the next checkpoint's diff.++ if (checkpoints && e.type === 'assistant' && ((e.message && e.message.content) || []).some((c) => c && c.type === 'tool_use')) {++ try {++ checkpoints.authored(toolPaths(e));++ checkpoints.capture(`turn ${e._attempt || 1}`);++ } catch (err) {++ say({ t: 'note', level: 'warning', text: `A checkpoint could not be written: ${scrub(err.message)}` });++ }++ }const c = compact(e, { pictures });for (const line of Array.isArray(c) ? c : c ? [c] : []) say(line);if (e.type === 'system' && e.subtype === 'init' && e.agent && !result.agents.includes(e.agent)) result.agents.push(e.agent);@@@ -806,6 +833,24 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tmif (job.kind === 'conductor' && last && last.session_id) result.session = String(last.session_id).slice(0, 200);// A conductor works in the owner's own folder: its changes are theirs, not a patch for a pull request.Object.assign(result, job.kind === 'code' ? await changes(work) : { files: [], patch: '', patch_truncated: false });++ if (checkpoints) {++ try {++ checkpoints.capture('final');++ const turns = checkpoints.turns();++ const baseline = checkpoints.entries[0];++ const final = checkpoints.last();++ result.checkpoints = {++ root: checkpoints.root,++ count: checkpoints.list().length,++ refs: checkpoints.list().slice(-20),++ // The turn diff: one row per turn, with the files the turn wrote split into authored and imported.++ turns: turns.slice(-20).map((t) => ({ ...t, files: t.files.slice(0, 20), truncated: t.files_total > 20 })),++ ...(baseline && final ? { turn_diff: checkpoints.diff(baseline, final) } : {}),++ };++ } catch (e) {++ say({ t: 'note', level: 'warning', text: `The checkpoints could not be read back: ${scrub(e.message)}` });++ }++ }say({ t: 'step', text: result.files.length ? `${result.files.length} file${result.files.length === 1 ? '' : 's'} changed` : (job.kind === 'conductor' ? 'Turn done' : 'No files changed') });// The proof of the change: the tests it reaches, the pages it touched, the diff, and a second model's opinion.// It is collected here, in the clone the agent worked in, so the evidence is about this machine's work.@@@ -588,9 +588,13 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou}// The job gets the files and the lines naming them, after the knowledge recall so it is in the same prompt;// the task row keeps the person's own words, so the page's "The brief" does not repeat the paths.-- const jobArgs = claims.length-- ? { ...runArgs, attachments: [...(Array.isArray(runArgs.attachments) ? runArgs.attachments : []), ...fileArgs(claims, turnId)], prompt: withFiles(runArgs.prompt, claims, turnId) }-- : runArgs;++ const jobArgs = {++ ...(claims.length++ ? { ...runArgs, attachments: [...(Array.isArray(runArgs.attachments) ? runArgs.attachments : []), ...fileArgs(claims, turnId)], prompt: withFiles(runArgs.prompt, claims, turnId) }++ : runArgs),++ // The harness's per-turn checkpoint refs name the task they belong to: refs/codes/checkpoints/<task>/<run>/<n>++ task_id: id,++ };const actorRefValue = actorRef && typeof actorRef === 'object' && !Array.isArray(actorRef) ? JSON.stringify(actorRef) : null;// Placement A1: the task's kind, its harness, and why the placer put it there. The person's rules, Bench priors// and learned counts are read once here; the placement itself is pure. A task whose task kind is review isnnew file mode 100644@@@ -0,0 +1,138 @@++'use strict';++/**++ * Per-turn checkpoints (harness/checkpoints.js, harness/job.js): a job's clone is pinned under++ * `refs/codes/checkpoints/<task>/<run>/<n>` before each tool batch, the turn diff splits the files the agent wrote++ * (authored) from the ones that came from elsewhere (imported), and a checkpoint can be restored. The job result++ * carries the refs and the diffs, never a push of them.++ */++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const { execFileSync } = require('child_process');++const { pathToFileURL } = require('url');++const { createCheckpoints, toolPaths } = require('../harness/checkpoints');++const { createHarness } = require('../harness');++const { runJob } = require('../harness/job');++const { check, done } = require('./helpers/boot');++++const git = (cwd, args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();++++(async () => {++ await check('a checkpoint is a git ref; the turn diff splits authored paths from imported ones', () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-cp-'));++ const repo = path.join(dir, 'repo');++ fs.mkdirSync(repo);++ git(repo, ['init', '-q']);++ fs.writeFileSync(path.join(repo, 'a.js'), 'const a = 1;\n');++ git(repo, ['add', '-A']);++ git(repo, ['-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-qm', 'base']);++ try {++ const cp = createCheckpoints({ cwd: repo, task: 'tsk_test', run: 'run_test' });++ const zero = cp.capture('start');++ assert.strictEqual(zero.n, 0);++ assert.strictEqual(zero.ref, 'refs/codes/checkpoints/tsk_test/run_test/0');++ assert.strictEqual(git(repo, ['rev-parse', '--verify', zero.ref]), zero.sha);++ assert.deepStrictEqual(cp.list().map((e) => e.n), [0]);++++ fs.writeFileSync(path.join(repo, 'a.js'), 'const a = 2;\n');++ cp.authored(['a.js']);++ const one = cp.capture('turn 1');++ fs.writeFileSync(path.join(repo, 'b.js'), 'imported\n');++ const two = cp.capture('turn 2');++++ const first = cp.diff(zero, one);++ assert.deepStrictEqual(first.files, [{ path: 'a.js', status: 'M', added: 1, removed: 1, authored: true }]);++ assert.deepStrictEqual(cp.diff(one, two).files, [{ path: 'b.js', status: 'A', added: 1, removed: 0, authored: false }]);++ assert.deepStrictEqual(cp.turns().map((t) => t.n), [1, 2]);++ assert.strictEqual(cp.turns()[1].authored, 0);++ assert.strictEqual(cp.turns()[1].imported, 1);++++ // Restore the last good checkpoint: a.js back, the imported file and an untracked broken one gone.++ fs.writeFileSync(path.join(repo, 'junk.js'), 'broken\n');++ assert.deepStrictEqual(cp.restore(zero), { n: 0, ref: zero.ref, sha: zero.sha });++ assert.strictEqual(fs.readFileSync(path.join(repo, 'a.js'), 'utf8'), 'const a = 1;\n');++ assert.strictEqual(fs.existsSync(path.join(repo, 'b.js')), false);++ assert.strictEqual(fs.existsSync(path.join(repo, 'junk.js')), false);++ assert.strictEqual(cp.restore(99), null);++ } finally {++ fs.rmSync(dir, { recursive: true, force: true });++ }++ });++++ await check('toolPaths reads the file a tool call names, never a command', () => {++ assert.deepStrictEqual(toolPaths({ type: 'assistant', message: { content: [++ { type: 'tool_use', input: { file_path: 'src/a.js' } },++ { type: 'tool_use', input: { path: 'src/b.js' } },++ { type: 'tool_use', input: { command: 'ls' } },++ { type: 'tool_use', input: { files: ['src/c.js', 7] } },++ ] } }), ['src/a.js', 'src/b.js', 'src/c.js']);++ assert.deepStrictEqual(toolPaths({ type: 'user', message: { content: [{ type: 'tool_result' }] } }), []);++ });++++ await check('a job carries its checkpoint refs and the final turn diff; the agent\'s own file is authored', async () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-cp-job-'));++ const repo = path.join(dir, 'repo');++ const bin = path.join(dir, 'bin');++ const home = path.join(dir, 'home');++ fs.mkdirSync(repo); fs.mkdirSync(bin);++ fs.writeFileSync(path.join(repo, 'a.js'), 'const a = 1;\n');++ git(repo, ['init', '-q']);++ git(repo, ['add', '-A']);++ git(repo, ['-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-qm', 'base']);++++ // Codex writes a file (it prints the file_change after writing, as the real CLI does), then hits its limit.++ // Claude Code takes over and finishes: the note is the agent's own work, not an import, whichever attempt did it.++ fs.writeFileSync(path.join(bin, 'codex'), `#!${process.execPath}++'use strict';++const fs = require('fs');++const out = (o) => console.log(JSON.stringify(o));++out({ type: 'thread.started', thread_id: 'cx-cp' });++out({ type: 'turn.started' });++fs.mkdirSync('notes', { recursive: true });++fs.writeFileSync('notes/turn.txt', 'written by codex\\n');++out({ type: 'item.completed', item: { id: 'i2', type: 'file_change', changes: [{ path: 'notes/turn.txt', kind: 'add' }], status: 'completed' } });++out({ type: 'error', message: 'You have hit your usage limit.' });++out({ type: 'turn.failed', error: { message: 'You have hit your usage limit.' } });++process.exit(1);++`, { mode: 0o755 });++ fs.writeFileSync(path.join(bin, 'claude'), `#!${process.execPath}++'use strict';++const out = (o) => console.log(JSON.stringify(o));++out({ type: 'system', subtype: 'init', session_id: 'cl-cp', model: 'fake-claude' });++out({ type: 'assistant', message: { content: [{ type: 'text', text: 'finished the note' }] } });++out({ type: 'result', subtype: 'success', is_error: false, result: 'finished the note', session_id: 'cl-cp' });++`, { mode: 0o755 });++++ const env = {++ ...process.env, PATH: bin, OPENVIBE_CODES_HOME: home, OPENVIBE_CODES_ALLOW_FILE_REPO: '1',++ DEEPSEEK_API_KEY: '', OPENVIBE_AI_TOKEN: '', OPENVIBE_APP_ID: '', OPENVIBE_APP_SECRET: '',++ };++ const harness = createHarness({ env });++ const lines = [];++ let doc = null;++ try {++ await runJob({++ input: JSON.stringify({ kind: 'code', repo: pathToFileURL(repo).href, prompt: 'Add the note', agent: 'codex', task_id: 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2T1' }),++ harness, env, emit: (line) => lines.push(JSON.parse(line)), writeResult: (d) => { doc = d; },++ });++ assert.ok(doc, 'the job wrote a result');++ // The agent hit its limit, so the job is not ok; the checkpoints of what it did are still there.++ assert.strictEqual(doc.ok, false);++ assert.deepStrictEqual(doc.agents, ['codex']);++ assert.ok(doc.checkpoints, 'the result carries its checkpoints');++ assert.match(doc.checkpoints.root, /^refs\/codes\/checkpoints\/tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2T1\/run_/);++ assert.ok(doc.checkpoints.count >= 2, `count ${doc.checkpoints.count}`);++ const note = doc.checkpoints.turns.flatMap((t) => t.files).find((f) => f.path === 'notes/turn.txt');++ assert.ok(note, JSON.stringify(doc.checkpoints.turns));++ assert.strictEqual(note.authored, true);++ assert.ok(doc.checkpoints.turn_diff.files.some((f) => f.path === 'notes/turn.txt' && f.authored === true));++ assert.ok(doc.checkpoints.turns.some((t) => t.files_total > 0));++ assert.ok(!lines.some((l) => l.t === 'note' && /Checkpoints are off/.test(l.text)), 'checkpoints did not fail');++ } finally {++ fs.rmSync(dir, { recursive: true, force: true });++ }++ });++++ done();++})().catch((e) => { console.error(e); process.exit(1); });@@@ -31,7 +31,7 @@ const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));await check('a manifest becomes one capability per field, and fits() reads it', () => {assert.deepStrictEqual(runtime.capabilities(runtime.forHarness('claude-code')), [-- 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-none',++ 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-git','harness:subagents-native', 'harness:context-handoff-session', 'harness:enforcement-native',]);assert.deepStrictEqual(runtime.requirements({ steering: 'resume', subagents: 'native' }), ['harness:steering-resume', 'harness:subagents-native']);@@@ -55,9 +55,9 @@ const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));for (const id of ['codex:codex-default', 'opencode:opencode-default', 'aider:aider-default', 'deepseek:deepseek-chat']) {assert.deepStrictEqual(r.candidates.find((c) => c.id === id), { id, included: false, reason: 'lacks harness:subagents-native' });}-- const none = route({ task: 'edit', extra: { capabilities: ['harness:checkpointing-git'] }, harnesses, now: Date.now() });-- assert.strictEqual(none.selected, null, 'no harness declares checkpointing yet');-- assert.ok(none.candidates.every((c) => c.reason === 'lacks harness:checkpointing-git'));++ const git = route({ task: 'edit', extra: { capabilities: ['harness:checkpointing-git'] }, harnesses, now: Date.now() });++ assert.ok(git.selected, 'checkpointing is declared by every catalog harness');++ assert.ok(git.candidates.every((c) => c.included), 'a job can checkpoint any harness');});await check('the tier router skips a ladder rung its manifest cannot meet', () => {@@@ -41,7 +41,7 @@ function priced(row, freshUsd) {region: 'global',trust: 'partner',capabilities: ['task:edit', 'harness:host-access', 'harness:mcp', 'harness:resume', 'harness:edit', 'harness:tools', 'runtime:code',-- 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-none',++ 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-git','harness:subagents-none', 'harness:context-handoff-session', 'harness:enforcement-client-boundary'],capacity: { workers: { harness: 2 } },health: { status: 'degraded' },@@@ -55,7 +55,7 @@ function priced(row, freshUsd) {const offer = toOffer(deepseek, deepseek.agents[0]);assert.strictEqual(offer.offer_id, 'deepseek:deepseek-chat');assert.deepStrictEqual(offer.capabilities, ['task:edit', 'task:review', 'harness:host-access', 'harness:resume', 'harness:edit', 'harness:review', 'harness:tools',-- 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-none',++ 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-git','harness:subagents-none', 'harness:context-handoff-session', 'harness:enforcement-client-boundary']);assert.strictEqual(offer.trust, 'external');assert.deepStrictEqual(offer.health, { status: 'up' });@@@ -78,7 +78,7 @@ function priced(row, freshUsd) {const browsing = { ...rows[0], task_capabilities: ['edit', 'browse'] };const offer = toOffer(browsing, browsing.agents[0], { now });assert.deepStrictEqual(offer.capabilities, ['task:edit', 'task:browse', 'harness:host-access', 'harness:mcp', 'harness:long-autonomy', 'harness:resume', 'harness:edit', 'harness:review', 'harness:tools', 'harness:vision', 'runtime:code',-- 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-none',++ 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-git','harness:subagents-native', 'harness:context-handoff-session', 'harness:enforcement-native']);assert.ok(contracts.validate('platform.resource-offer@1', JSON.parse(JSON.stringify(offer))).valid);});ddeleted file mode 100644@@@ -1,77 +0,0 @@--'use strict';--/**-- * The runtime capability manifest (engine/runtime.js): every catalog harness declares what it can do, every offer-- * carries it as `harness:<field>-<value>` capabilities, and both routers (placement and the tier router) exclude a-- * harness that does not declare what the task needs — no provider-name conditional in either.-- */--const assert = require('assert');--const fs = require('fs');--const path = require('path');--const { createHarnesses } = require('../server/domain/harnesses');--const { toOffer, route } = require('../server/domain/harness-placement');--const runtime = require('../engine/runtime');--const { pick } = require('../engine/router');--const { check, done } = require('./helpers/boot');----const catalogPath = path.join(__dirname, '..', 'server', 'data', 'harness-offers.json');--const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));----(async () => {-- await check('every catalog harness declares every field, with a known value', () => {-- for (const row of rows) {-- const manifest = runtime.forHarness(row.id);-- assert.ok(manifest, `${row.id} has no runtime manifest`);-- assert.deepStrictEqual(Object.keys(manifest).sort(), Object.keys(runtime.FIELDS).sort(), row.id);-- for (const [field, values] of Object.entries(runtime.FIELDS)) {-- assert.ok(values.includes(manifest[field]), `${row.id}.${field}: ${manifest[field]} is not one of ${values.join('|')}`);-- }-- }-- assert.deepStrictEqual(Object.keys(runtime.MANIFEST).sort(), rows.map((row) => row.id).sort(), 'no manifest without a catalog row');-- });---- await check('a manifest becomes one capability per field, and fits() reads it', () => {-- assert.deepStrictEqual(runtime.capabilities(runtime.forHarness('claude-code')), [-- 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-none',-- 'harness:subagents-native', 'harness:context-handoff-session', 'harness:enforcement-native',-- ]);-- assert.deepStrictEqual(runtime.requirements({ steering: 'resume', subagents: 'native' }), ['harness:steering-resume', 'harness:subagents-native']);-- assert.strictEqual(runtime.fits('claude-code', { subagents: 'native' }), true);-- assert.strictEqual(runtime.fits('codex', { subagents: 'native' }), false);-- assert.strictEqual(runtime.fits('cx', ['harness:steering-resume']), true, 'the portal runner id names the same harness');-- assert.strictEqual(runtime.fits('pg', ['harness:steering-resume']), false, 'an unknown runner declares nothing');-- assert.strictEqual(runtime.fits('pg', []), true, 'nothing required, nothing to exclude');-- });---- await check('every offer carries the manifest as capabilities the placement router can require', () => {-- const harnesses = createHarnesses({ catalogPath });-- for (const harness of harnesses.list()) {-- const offer = toOffer(harness, harnesses.agents(harness.id)[0]);-- for (const cap of runtime.capabilities(runtime.forHarness(harness.id))) {-- assert.ok(offer.capabilities.includes(cap), `${offer.offer_id} lacks ${cap}`);-- }-- }-- const r = route({ task: 'edit', extra: { capabilities: ['harness:subagents-native'] }, harnesses, now: Date.now() });-- assert.strictEqual(r.selected.split(':')[0], 'claude-code');-- for (const id of ['codex:codex-default', 'opencode:opencode-default', 'aider:aider-default', 'deepseek:deepseek-chat']) {-- assert.deepStrictEqual(r.candidates.find((c) => c.id === id), { id, included: false, reason: 'lacks harness:subagents-native' });-- }-- const none = route({ task: 'edit', extra: { capabilities: ['harness:checkpointing-git'] }, harnesses, now: Date.now() });-- assert.strictEqual(none.selected, null, 'no harness declares checkpointing yet');-- assert.ok(none.candidates.every((c) => c.reason === 'lacks harness:checkpointing-git'));-- });---- await check('the tier router skips a ladder rung its manifest cannot meet', () => {-- const ctx = {-- cfg: { subagents: { mm: { enabled: true, max: 2 }, cm: { enabled: true, max: 2 } } },-- health: { mm: { state: 'ok' }, cm: { state: 'ok' } },-- counts: {}, models: {}, runs: [],-- ladder: { basic: [['mm', null, 'high'], ['cm', null, 'high']] },-- };-- assert.strictEqual(pick('Fix the parser', ctx).runner, 'mm', 'no requirement: the first rung stays');-- assert.strictEqual(pick('Fix the parser', { ...ctx, needs: ['harness:subagents-native'] }).runner, 'cm');-- assert.strictEqual(pick('Fix the parser', { ...ctx, needs: { steering: 'resume' } }).runner, 'mm');-- assert.strictEqual(pick('Fix the parser', { ...ctx, needs: ['harness:subagents-native'], ladder: { basic: [['mm', null, 'high']] } }), null);-- });---- done();--})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
+# Per-turn checkpoints
+
+A coding job clones the repository into a scratch directory and an agent works there. `harness/checkpoints.js` pins
+that worktree to a git commit before each tool batch and keeps it under a ref:
+
+```
+refs/codes/checkpoints/<task>/<run>/<n>
+```
+
+The refs are the run's own history. They are never pushed and never become a branch, and the clone (and so the refs)
+is removed when the job ends. What survives is the job result: the refs, each turn's diff, and the final turn diff
+against the clone as the agent found it.
+
+## How a checkpoint is written
+
+A checkpoint uses a temporary index (`GIT_INDEX_FILE`) so it never touches the run's own index:
+
+```
+read-tree HEAD → add -A → write-tree → commit-tree -p <previous checkpoint> → update-ref
+```
+
+`.gitignore` and `.git/info/exclude` still apply, so `node_modules`, build output and the owner's `.openvibe/` files
+stay out. `n = 0` is the clone as the agent found it; the job takes one more at the end (`final`) for the last turn.
+
+## The turn diff
+
+`checkpoints.turns[]` has one row per checkpoint after the start:
+
+```json
+{
+ "n": 2, "ref": "refs/codes/checkpoints/tsk_…/run_…/2", "label": "turn 1",
+ "from": 1, "to": 2, "files_total": 3, "authored": 2, "imported": 1, "truncated": false,
+ "files": [
+ { "path": "src/user.js", "status": "M", "added": 4, "removed": 3, "authored": true },
+ { "path": "package-lock.json", "status": "M", "added": 120, "removed": 2, "authored": false }
+ ]
+}
+```
+
+A file is **authored** when a tool call named it during the run (Claude Code's tool vocabulary, whatever harness ran)
+and **imported** otherwise: a patch applied from an earlier conversation, a dependency install, a formatter, a
+subagent's work. `status` is git's letter (`A`, `M`, `D`, `R`); `checkpoints.turn_diff` is the whole run against
+`n = 0`. A result carries at most 20 turns and 20 files a turn (`files_total` says how many there really were).
+
+## Restoring
+
+`createCheckpoints(...).restore(n)` puts the worktree back to a checkpoint: `read-tree --reset -u` of its tree, then
+`git clean -fdq` for the untracked files a broken turn left (ignored files stay). The last checkpoint is the last good
+state before the current turn. This is the operation a **failed repair** uses: the repair loop (After-Loops AL2, the
+same builder resumed with findings) restores the last good turn before it retries, rather than asking the model to
+re-read and re-edit from a broken tree. Routine hand-offs do **not** restore: when an agent runs out of its plan
+mid-work, the next agent keeps that work (the hand-off note tells it what was done).
+
+The module is tested in `test/checkpoints.test.js`; a job's integration (refs written, the agent's own file marked
+authored, the failed run still carrying its diffs) runs in the same file against a real local clone.
@@ -35,19 +35,20 @@ nothing, and is excluded only when a requirement is set.
@@ -35,19 +35,20 @@ nothing, and is excluded only when a requirement is set.
| harness | terminal_status | steering | checkpointing | subagents | context_handoff | enforcement |
| harness | terminal_status | steering | checkpointing | subagents | context_handoff | enforcement |
|---|---|---|---|---|---|---|
|---|---|---|---|---|---|---|
-| `claude-code` | structured | resume | none | native | session | native |
-| `claude-other` | structured | resume | none | native | session | native |
-| `codex` | structured | resume | none | none | session | native |
-| `opencode` | structured | resume | none | none | session | client-boundary |
-| `command-code` | structured | resume | none | none | session | client-boundary |
-| `aider` | parsed | none | none | none | none | client-boundary |
-| `minimax-code` | structured | resume | none | none | session | native |
-| `deepseek` | structured | resume | none | none | session | client-boundary |
-| `openvibe-ai` | structured | resume | none | none | session | client-boundary |
-| `openai-compatible` | structured | resume | none | none | session | client-boundary |
+| `claude-code` | structured | resume | git | native | session | native |
+| `claude-other` | structured | resume | git | native | session | native |
+| `codex` | structured | resume | git | none | session | native |
+| `opencode` | structured | resume | git | none | session | client-boundary |
+| `command-code` | structured | resume | git | none | session | client-boundary |
+| `aider` | parsed | none | git | none | none | client-boundary |
+| `minimax-code` | structured | resume | git | none | session | native |
+| `deepseek` | structured | resume | git | none | session | client-boundary |
+| `openvibe-ai` | structured | resume | git | none | session | client-boundary |
+| `openai-compatible` | structured | resume | git | none | session | client-boundary |
-`checkpointing` flips to `git` on every row when the per-turn checkpoint refs land (`harness/job.js`): the manifest is
-the runtime's declaration, so it must not claim a checkpoint before one exists.
+`checkpointing` is `git` on every row: a job pins its clone to `refs/codes/checkpoints/<task>/<run>/<n>` per turn
+(`harness/job.js`, `harness/checkpoints.js`) and can restore the last good one; the refs are the run's own and are
+never pushed.
## Using it
## Using it
@@ -65,48 +65,49 @@ const CAPABILITY = Object.freeze({
@@ -65,48 +65,49 @@ const CAPABILITY = Object.freeze({
*
*
* Grounded in each adapter (harness/adapters/cli.js, harness/adapters/api.js) and its CLI's flags, not in its name:
* Grounded in each adapter (harness/adapters/cli.js, harness/adapters/api.js) and its CLI's flags, not in its name:
* every CLI here ends with a result line except Aider (its prose is parsed); every CLI with `--resume`/`--session`/`-s`
* every CLI here ends with a result line except Aider (its prose is parsed); every CLI with `--resume`/`--session`/`-s`
- * continues its session; Claude Code alone exposes subagents as tools. `checkpointing` is `none` until the per-turn
- * checkpoint refs land: this manifest is the runtime's declaration, so it must not claim one early.
+ * continues its session; Claude Code alone exposes subagents as tools. `checkpointing` is `git` for every harness:
+ * the job pins its clone to `refs/codes/checkpoints/<task>/<run>/<n>` per turn (harness/checkpoints.js), whatever the
+ * harness.
*/
*/
const MANIFEST = Object.freeze({
const MANIFEST = Object.freeze({
'claude-code': Object.freeze({
'claude-code': Object.freeze({
- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',
+ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',
subagents: 'native', context_handoff: 'session', enforcement: 'native',
subagents: 'native', context_handoff: 'session', enforcement: 'native',
}),
}),
'claude-other': Object.freeze({
'claude-other': Object.freeze({
- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',
+ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',
subagents: 'native', context_handoff: 'session', enforcement: 'native',
subagents: 'native', context_handoff: 'session', enforcement: 'native',
}),
}),
codex: Object.freeze({
codex: Object.freeze({
- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',
+ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',
subagents: 'none', context_handoff: 'session', enforcement: 'native',
subagents: 'none', context_handoff: 'session', enforcement: 'native',
}),
}),
opencode: Object.freeze({
opencode: Object.freeze({
- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',
+ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',
subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',
subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',
}),
}),
'command-code': Object.freeze({
'command-code': Object.freeze({
- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',
+ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',
subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',
subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',
}),
}),
aider: Object.freeze({
aider: Object.freeze({
- terminal_status: 'parsed', steering: 'none', checkpointing: 'none',
+ terminal_status: 'parsed', steering: 'none', checkpointing: 'git',
subagents: 'none', context_handoff: 'none', enforcement: 'client-boundary',
subagents: 'none', context_handoff: 'none', enforcement: 'client-boundary',
}),
}),
'minimax-code': Object.freeze({
'minimax-code': Object.freeze({
- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',
+ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',
subagents: 'none', context_handoff: 'session', enforcement: 'native',
subagents: 'none', context_handoff: 'session', enforcement: 'native',
}),
}),
deepseek: Object.freeze({
deepseek: Object.freeze({
- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',
+ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',
subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',
subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',
}),
}),
'openvibe-ai': Object.freeze({
'openvibe-ai': Object.freeze({
- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',
+ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',
subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',
subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',
}),
}),
'openai-compatible': Object.freeze({
'openai-compatible': Object.freeze({
- terminal_status: 'structured', steering: 'resume', checkpointing: 'none',
+ terminal_status: 'structured', steering: 'resume', checkpointing: 'git',
subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',
subagents: 'none', context_handoff: 'session', enforcement: 'client-boundary',
}),
}),
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,209 @@
@@ -0,0 +1,209 @@
+'use strict';
+
+/**
+ * Per-turn checkpoints in a job's clone: before each model turn (really before each tool batch, which is the closest
+ * boundary the stream shows) the working tree is written to a git commit and pinned under
+ * `refs/codes/checkpoints/<task>/<run>/<n>`. Nothing is committed to a branch and nothing is pushed: the refs are the
+ * run's own history, and the job's clone is thrown away after it.
+ *
+ * const cp = createCheckpoints({ cwd, task, run });
+ * cp.capture('start'); // n = 0, the clone as the agent found it
+ * cp.authored(['src/user.js']); // paths this turn's tools wrote
+ * cp.capture('turn'); // n = 1, after the turn
+ * cp.diff(0, 1); // { files: [{ path, status, added, removed, authored }], … }
+ * cp.restore(0); // put the worktree back to the last good turn (the repair path)
+ *
+ * The diff marks a path **authored** when a tool call named it in the turn and **imported** otherwise (a patch applied,
+ * a dependency install, a formatter or a subagent): the first is the agent's work, the second is something that came
+ * from elsewhere, and a review should be told them apart.
+ *
+ * A checkpoint is built with a temporary index (`GIT_INDEX_FILE`) so it never touches the run's own index:
+ * `read-tree HEAD` → `add -A` → `write-tree` → `commit-tree` with the previous checkpoint as parent → `update-ref`.
+ * `.gitignore` and `.git/info/exclude` still apply, so build output, node_modules and the owner's `.openvibe/` files
+ * stay out.
+ */
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { execFileSync } = require('child_process');
+
+/** A cap on a run's refs and on a diff's rows, so a very long turn cannot flood the result. */
+const MAX_TURNS = 200;
+const MAX_FILES = 100;
+
+/** git in the worktree, no prompt and no credential helper; a failure names the command and the last line. */
+function git(cwd, args, env = {}) {
+ try {
+ return execFileSync('git', args, {
+ cwd, encoding: 'utf8', timeout: 30_000, maxBuffer: 16 * 1024 * 1024,
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo', ...env },
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+ } catch (error) {
+ const said = String(error.stderr || error.message || '').trim().split('\n').slice(-2).join(' ');
+ const err = new Error(`checkpoint: git ${args[0]} failed: ${said}`);
+ err.code = 'checkpoint.git';
+ throw err;
+ }
+}
+
+/** A ref path component that git accepts and that a person can read back (a task id is already safe). */
+function safeName(value) {
+ return String(value == null ? '' : value).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80) || 'run';
+}
+
+/** The file paths a harness event wrote, from its tool calls (Claude Code's names are the shared vocabulary). */
+function toolPaths(event) {
+ const out = [];
+ if (!event || event.type !== 'assistant') return out;
+ for (const c of (event.message && event.message.content) || []) {
+ if (!c || c.type !== 'tool_use') continue;
+ const i = c.input && typeof c.input === 'object' ? c.input : {};
+ for (const key of ['file_path', 'path', 'notebook_path']) if (typeof i[key] === 'string' && i[key]) out.push(i[key]);
+ // A patch helper names its files in a list; an edit may carry a list of edits for one path.
+ if (Array.isArray(i.files)) out.push(...i.files.filter((f) => typeof f === 'string'));
+ }
+ return out;
+}
+
+/**
+ * The checkpoints of one run.
+ *
+ * @param {object} opts
+ * @param {string} opts.cwd the clone
+ * @param {string} [opts.task] the task id (the ref's first component)
+ * @param {string} [opts.run] the run id (the ref's second component)
+ * @param {number} [opts.maxTurns] the most refs one run writes
+ * @param {number} [opts.maxFiles] the most rows one diff carries
+ * @param {() => number} [opts.now]
+ */
+function createCheckpoints({ cwd, task = 'task', run = 'run', maxTurns = MAX_TURNS, maxFiles = MAX_FILES, now = Date.now } = {}) {
+ const root = `refs/codes/checkpoints/${safeName(task)}/${safeName(run)}`;
+ const entries = [];
+ const pending = new Set();
+ // Every path a tool has named in this run: a diff marks a file authored if the agent touched it, however many
+ // turns the write and the event that reports it fall apart by (a CLI prints the call before or after it runs).
+ const named = new Set();
+
+ const head = () => git(cwd, ['rev-parse', 'HEAD']).trim();
+
+ /**
+ * Capture the worktree now as checkpoint `n`. The parent is the previous checkpoint (or HEAD for the first), so the
+ * chain is inspectable with `git log`. The paths tools wrote since the previous capture are this checkpoint's
+ * authored set.
+ *
+ * @param {string} [label] what the capture marks (start, turn, final)
+ * @returns {object|null} the entry, or null once the cap is reached
+ */
+ function capture(label = 'turn') {
+ if (entries.length >= maxTurns) return null;
+ const n = entries.length;
+ const index = path.join(os.tmpdir(), `codes-index-${process.pid}-${Date.now()}-${n}`);
+ const env = { GIT_INDEX_FILE: index };
+ try {
+ git(cwd, ['read-tree', 'HEAD'], env);
+ git(cwd, ['add', '-A'], env);
+ const tree = git(cwd, ['write-tree'], env).trim();
+ const parent = entries.length ? entries[entries.length - 1].sha : head();
+ const sha = git(cwd, [
+ '-c', 'user.name=OpenVibe.Codes', '-c', '[email protected]',
+ 'commit-tree', tree, '-p', parent, '-m', `codes checkpoint ${n} ${label}`,
+ ]).trim();
+ const ref = `${root}/${n}`;
+ git(cwd, ['update-ref', ref, sha]);
+ const entry = { n, ref, sha, tree, label, at: new Date(now()).toISOString(), authored: [...pending] };
+ pending.clear();
+ entries.push(entry);
+ return entry;
+ } finally {
+ try { fs.rmSync(index, { force: true }); } catch { /* the temp index is best effort */ }
+ }
+ }
+
+ /** The paths the current turn's tools wrote: a per-turn snapshot, and the run's cumulative authored set. */
+ function authored(paths) {
+ for (const p of paths || []) if (p) { pending.add(String(p)); named.add(String(p)); }
+ }
+
+ /** Resolve a checkpoint by number, ref or sha. */
+ function entryOf(which) {
+ if (which && typeof which === 'object') return which;
+ return entries.find((e) => e.n === Number(which) || e.ref === which || e.sha === which) || null;
+ }
+
+ /**
+ * The diff one turn made, with its paths split by provenance.
+ *
+ * @param {number|string|object} from checkpoint n / ref / sha
+ * @param {number|string|object} to checkpoint n / ref / sha
+ * @returns {{from: number|null, to: number|null, files: object[], files_total: number, authored: number, imported: number}}
+ */
+ function diff(from, to) {
+ const a = entryOf(from);
+ const b = entryOf(to);
+ const empty = { from: a ? a.n : null, to: b ? b.n : null, files: [], files_total: 0, authored: 0, imported: 0 };
+ if (!a || !b) return empty;
+ const status = git(cwd, ['diff', '--name-status', '--find-renames', a.sha, b.sha]).trim();
+ if (!status) return empty;
+ const stats = new Map();
+ for (const row of git(cwd, ['diff', '--numstat', a.sha, b.sha]).trim().split('\n').filter(Boolean)) {
+ const [added, removed, p] = row.split('\t');
+ if (p == null) continue;
+ // A rename's numstat path is `old => new` (or `{old => new}` for a shared prefix): keep the newest path.
+ const file = p.includes(' => ') ? p.replace(/^.* => /, '').replace(/[{}]/g, '').trim() : p;
+ stats.set(file, { added: Number(added) || 0, removed: Number(removed) || 0 });
+ }
+ const authoredSet = named;
+ const files = [];
+ for (const line of status.split('\n')) {
+ const parts = line.split('\t');
+ const code = String(parts[0] || '').charAt(0);
+ const file = code === 'R' || code === 'C' ? parts[2] : parts[1];
+ if (!file) continue;
+ const nums = stats.get(file) || { added: 0, removed: 0 };
+ files.push({ path: file, status: code, added: nums.added, removed: nums.removed, authored: authoredSet.has(file) || (code === 'R' && authoredSet.has(parts[1])) });
+ }
+ const authored = files.filter((f) => f.authored).length;
+ return { from: a.n, to: b.n, files: files.slice(0, maxFiles), files_total: files.length, authored, imported: files.length - authored };
+ }
+
+ /**
+ * Put the worktree back to a checkpoint: index and tracked files from its tree, then the untracked files a broken
+ * turn left are cleaned (ignored files, including `.openvibe/`, stay).
+ *
+ * @param {number|string|object} which checkpoint n / ref / sha
+ * @returns {{n: number, ref: string, sha: string}|null}
+ */
+ function restore(which) {
+ const entry = entryOf(which);
+ if (!entry) return null;
+ git(cwd, ['read-tree', '--reset', '-u', entry.sha]);
+ try { git(cwd, ['clean', '-fdq']); } catch { /* a clean failure leaves the restored tree as is */ }
+ return { n: entry.n, ref: entry.ref, sha: entry.sha };
+ }
+
+ /** The refs written, oldest first, without the paths (a result carries the diffs separately). */
+ function list() {
+ return entries.map((e) => ({ n: e.n, ref: e.ref, sha: e.sha, label: e.label, at: e.at }));
+ }
+
+ /** The last checkpoint (the last good state before the current turn). */
+ function last() {
+ return entries[entries.length - 1] || null;
+ }
+
+ /** Every turn's diff from the previous checkpoint, one row per ref after the first. */
+ function turns() {
+ const out = [];
+ for (let i = 1; i < entries.length; i++) {
+ const d = diff(entries[i - 1], entries[i]);
+ out.push({ n: entries[i].n, ref: entries[i].ref, sha: entries[i].sha, label: entries[i].label, at: entries[i].at, ...d });
+ }
+ return out;
+ }
+
+ return { root, capture, authored, diff, restore, list, last, turns, entries, safeName };
+}
+
+module.exports = { createCheckpoints, toolPaths, safeName, MAX_TURNS, MAX_FILES };
@@ -9,15 +9,17 @@
@@ -9,15 +9,17 @@
*
*
* It clones `repo` (https only) into a scratch directory, runs the harness there with hand-offs, and prints one compact
* It clones `repo` (https only) into a scratch directory, runs the harness there with hand-offs, and prints one compact
* JSON line per event on stdout (what the page shows live). The result goes to fd 3 (the Link's result channel), or as
* JSON line per event on stdout (what the page shows live). The result goes to fd 3 (the Link's result channel), or as
- * a last `{ t: 'result_doc' }` line when fd 3 is not open: what the agents said, the changed files and the patch, and,
- * when `push` is asked and the machine holds GITHUB_TOKEN, the new branch it pushed (never the base branch) and its
- * pull request. Nothing here ever prints a token.
+ * a last `{ t: 'result_doc' }` line when fd 3 is not open: what the agents said, the changed files and the patch, the
+ * per-turn checkpoints (refs and the authored/imported turn diff, `checkpoints` in the document), and, when `push` is
+ * asked and the machine holds GITHUB_TOKEN, the new branch it pushed (never the base branch) and its pull request.
+ * Nothing here ever prints a token.
*/
*/
const fs = require('fs');
const fs = require('fs');
const os = require('os');
const os = require('os');
const path = require('path');
const path = require('path');
const { execFile } = require('child_process');
const { execFile } = require('child_process');
const { collectProof } = require('../engine/proof');
const { collectProof } = require('../engine/proof');
+const { createCheckpoints, toolPaths } = require('./checkpoints');
const JOB_PROTOCOL = '1.0.0';
const JOB_PROTOCOL = '1.0.0';
/** The agents a conductor's turn may run on: every harness Codes offers. */
/** The agents a conductor's turn may run on: every harness Codes offers. */
@@ -248,6 +250,8 @@ function parseArgs(raw, env = process.env) {
@@ -248,6 +250,8 @@ function parseArgs(raw, env = process.env) {
} : null;
} : null;
return {
return {
kind, repo: repo ? repo.toString() : null, ref, prompt, permission, push,
kind, repo: repo ? repo.toString() : null, ref, prompt, permission, push,
+ // The task this job is an attempt of, when the server names it: the checkpoint refs carry it.
+ ...(typeof a.task_id === 'string' && /^tsk_[0-9A-HJKMNP-TV-Z]{26}$/.test(a.task_id) ? { taskId: a.task_id } : {}),
...(attached.length ? { attachments: attached } : {}),
...(attached.length ? { attachments: attached } : {}),
...(limits ? { limits } : {}),
...(limits ? { limits } : {}),
...(resume ? { resume } : {}),
...(resume ? { resume } : {}),
@@ -782,11 +786,34 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
@@ -782,11 +786,34 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
resume: job.resume || null,
resume: job.resume || null,
...(adapterOptions ? { adapterOptions } : {}),
...(adapterOptions ? { adapterOptions } : {}),
});
});
+ // Per-turn checkpoints (Release 1, feat 11): the clone as the agent found it, then one ref per tool batch. They
+ // live in this clone only and are never pushed; the result carries the refs and the turn diffs so a review can
+ // see the agent's own work apart from what came from elsewhere. A checkpoint failure never fails the run.
+ let checkpoints = null;
+ if (job.kind === 'code') {
+ try {
+ checkpoints = createCheckpoints({ cwd: work, task: job.taskId || 'task', run: env.OPENVIBE_JOB_ID || run.runId });
+ checkpoints.capture('start');
+ } catch (e) {
+ say({ t: 'note', level: 'warning', text: `Checkpoints are off for this run: ${scrub(e.message)}` });
+ checkpoints = null;
+ }
+ }
let last = null;
let last = null;
// What the pictures may take of the result's room.
// What the pictures may take of the result's room.
const pictures = { left: MAX_RESULT_BYTES };
const pictures = { left: MAX_RESULT_BYTES };
for await (const e of run) {
for await (const e of run) {
last = e;
last = e;
+ // Before the tool batch the model just called runs, pin the tree: this is the checkpoint a failed repair
+ // returns to. The paths the batch names become the authored set of the next checkpoint's diff.
+ if (checkpoints && e.type === 'assistant' && ((e.message && e.message.content) || []).some((c) => c && c.type === 'tool_use')) {
+ try {
+ checkpoints.authored(toolPaths(e));
+ checkpoints.capture(`turn ${e._attempt || 1}`);
+ } catch (err) {
+ say({ t: 'note', level: 'warning', text: `A checkpoint could not be written: ${scrub(err.message)}` });
+ }
+ }
const c = compact(e, { pictures });
const c = compact(e, { pictures });
for (const line of Array.isArray(c) ? c : c ? [c] : []) say(line);
for (const line of Array.isArray(c) ? c : c ? [c] : []) say(line);
if (e.type === 'system' && e.subtype === 'init' && e.agent && !result.agents.includes(e.agent)) result.agents.push(e.agent);
if (e.type === 'system' && e.subtype === 'init' && e.agent && !result.agents.includes(e.agent)) result.agents.push(e.agent);
@@ -806,6 +833,24 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
@@ -806,6 +833,24 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
if (job.kind === 'conductor' && last && last.session_id) result.session = String(last.session_id).slice(0, 200);
if (job.kind === 'conductor' && last && last.session_id) result.session = String(last.session_id).slice(0, 200);
// A conductor works in the owner's own folder: its changes are theirs, not a patch for a pull request.
// A conductor works in the owner's own folder: its changes are theirs, not a patch for a pull request.
Object.assign(result, job.kind === 'code' ? await changes(work) : { files: [], patch: '', patch_truncated: false });
Object.assign(result, job.kind === 'code' ? await changes(work) : { files: [], patch: '', patch_truncated: false });
+ if (checkpoints) {
+ try {
+ checkpoints.capture('final');
+ const turns = checkpoints.turns();
+ const baseline = checkpoints.entries[0];
+ const final = checkpoints.last();
+ result.checkpoints = {
+ root: checkpoints.root,
+ count: checkpoints.list().length,
+ refs: checkpoints.list().slice(-20),
+ // The turn diff: one row per turn, with the files the turn wrote split into authored and imported.
+ turns: turns.slice(-20).map((t) => ({ ...t, files: t.files.slice(0, 20), truncated: t.files_total > 20 })),
+ ...(baseline && final ? { turn_diff: checkpoints.diff(baseline, final) } : {}),
+ };
+ } catch (e) {
+ say({ t: 'note', level: 'warning', text: `The checkpoints could not be read back: ${scrub(e.message)}` });
+ }
+ }
say({ t: 'step', text: result.files.length ? `${result.files.length} file${result.files.length === 1 ? '' : 's'} changed` : (job.kind === 'conductor' ? 'Turn done' : 'No files changed') });
say({ t: 'step', text: result.files.length ? `${result.files.length} file${result.files.length === 1 ? '' : 's'} changed` : (job.kind === 'conductor' ? 'Turn done' : 'No files changed') });
// The proof of the change: the tests it reaches, the pages it touched, the diff, and a second model's opinion.
// The proof of the change: the tests it reaches, the pages it touched, the diff, and a second model's opinion.
// It is collected here, in the clone the agent worked in, so the evidence is about this machine's work.
// It is collected here, in the clone the agent worked in, so the evidence is about this machine's work.
@@ -588,9 +588,13 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
@@ -588,9 +588,13 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
}
}
// The job gets the files and the lines naming them, after the knowledge recall so it is in the same prompt;
// The job gets the files and the lines naming them, after the knowledge recall so it is in the same prompt;
// the task row keeps the person's own words, so the page's "The brief" does not repeat the paths.
// the task row keeps the person's own words, so the page's "The brief" does not repeat the paths.
- const jobArgs = claims.length
- ? { ...runArgs, attachments: [...(Array.isArray(runArgs.attachments) ? runArgs.attachments : []), ...fileArgs(claims, turnId)], prompt: withFiles(runArgs.prompt, claims, turnId) }
- : runArgs;
+ const jobArgs = {
+ ...(claims.length
+ ? { ...runArgs, attachments: [...(Array.isArray(runArgs.attachments) ? runArgs.attachments : []), ...fileArgs(claims, turnId)], prompt: withFiles(runArgs.prompt, claims, turnId) }
+ : runArgs),
+ // The harness's per-turn checkpoint refs name the task they belong to: refs/codes/checkpoints/<task>/<run>/<n>
+ task_id: id,
+ };
const actorRefValue = actorRef && typeof actorRef === 'object' && !Array.isArray(actorRef) ? JSON.stringify(actorRef) : null;
const actorRefValue = actorRef && typeof actorRef === 'object' && !Array.isArray(actorRef) ? JSON.stringify(actorRef) : null;
// Placement A1: the task's kind, its harness, and why the placer put it there. The person's rules, Bench priors
// Placement A1: the task's kind, its harness, and why the placer put it there. The person's rules, Bench priors
// and learned counts are read once here; the placement itself is pure. A task whose task kind is review is
// and learned counts are read once here; the placement itself is pure. A task whose task kind is review is
new file mode 100644
new file mode 100644
@@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
+'use strict';
+/**
+ * Per-turn checkpoints (harness/checkpoints.js, harness/job.js): a job's clone is pinned under
+ * `refs/codes/checkpoints/<task>/<run>/<n>` before each tool batch, the turn diff splits the files the agent wrote
+ * (authored) from the ones that came from elsewhere (imported), and a checkpoint can be restored. The job result
+ * carries the refs and the diffs, never a push of them.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { execFileSync } = require('child_process');
+const { pathToFileURL } = require('url');
+const { createCheckpoints, toolPaths } = require('../harness/checkpoints');
+const { createHarness } = require('../harness');
+const { runJob } = require('../harness/job');
+const { check, done } = require('./helpers/boot');
+
+const git = (cwd, args) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
+
+(async () => {
+ await check('a checkpoint is a git ref; the turn diff splits authored paths from imported ones', () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-cp-'));
+ const repo = path.join(dir, 'repo');
+ fs.mkdirSync(repo);
+ git(repo, ['init', '-q']);
+ fs.writeFileSync(path.join(repo, 'a.js'), 'const a = 1;\n');
+ git(repo, ['add', '-A']);
+ git(repo, ['-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-qm', 'base']);
+ try {
+ const cp = createCheckpoints({ cwd: repo, task: 'tsk_test', run: 'run_test' });
+ const zero = cp.capture('start');
+ assert.strictEqual(zero.n, 0);
+ assert.strictEqual(zero.ref, 'refs/codes/checkpoints/tsk_test/run_test/0');
+ assert.strictEqual(git(repo, ['rev-parse', '--verify', zero.ref]), zero.sha);
+ assert.deepStrictEqual(cp.list().map((e) => e.n), [0]);
+
+ fs.writeFileSync(path.join(repo, 'a.js'), 'const a = 2;\n');
+ cp.authored(['a.js']);
+ const one = cp.capture('turn 1');
+ fs.writeFileSync(path.join(repo, 'b.js'), 'imported\n');
+ const two = cp.capture('turn 2');
+
+ const first = cp.diff(zero, one);
+ assert.deepStrictEqual(first.files, [{ path: 'a.js', status: 'M', added: 1, removed: 1, authored: true }]);
+ assert.deepStrictEqual(cp.diff(one, two).files, [{ path: 'b.js', status: 'A', added: 1, removed: 0, authored: false }]);
+ assert.deepStrictEqual(cp.turns().map((t) => t.n), [1, 2]);
+ assert.strictEqual(cp.turns()[1].authored, 0);
+ assert.strictEqual(cp.turns()[1].imported, 1);
+
+ // Restore the last good checkpoint: a.js back, the imported file and an untracked broken one gone.
+ fs.writeFileSync(path.join(repo, 'junk.js'), 'broken\n');
+ assert.deepStrictEqual(cp.restore(zero), { n: 0, ref: zero.ref, sha: zero.sha });
+ assert.strictEqual(fs.readFileSync(path.join(repo, 'a.js'), 'utf8'), 'const a = 1;\n');
+ assert.strictEqual(fs.existsSync(path.join(repo, 'b.js')), false);
+ assert.strictEqual(fs.existsSync(path.join(repo, 'junk.js')), false);
+ assert.strictEqual(cp.restore(99), null);
+ } finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+ });
+
+ await check('toolPaths reads the file a tool call names, never a command', () => {
+ assert.deepStrictEqual(toolPaths({ type: 'assistant', message: { content: [
+ { type: 'tool_use', input: { file_path: 'src/a.js' } },
+ { type: 'tool_use', input: { path: 'src/b.js' } },
+ { type: 'tool_use', input: { command: 'ls' } },
+ { type: 'tool_use', input: { files: ['src/c.js', 7] } },
+ ] } }), ['src/a.js', 'src/b.js', 'src/c.js']);
+ assert.deepStrictEqual(toolPaths({ type: 'user', message: { content: [{ type: 'tool_result' }] } }), []);
+ });
+
+ await check('a job carries its checkpoint refs and the final turn diff; the agent\'s own file is authored', async () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-cp-job-'));
+ const repo = path.join(dir, 'repo');
+ const bin = path.join(dir, 'bin');
+ const home = path.join(dir, 'home');
+ fs.mkdirSync(repo); fs.mkdirSync(bin);
+ fs.writeFileSync(path.join(repo, 'a.js'), 'const a = 1;\n');
+ git(repo, ['init', '-q']);
+ git(repo, ['add', '-A']);
+ git(repo, ['-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-qm', 'base']);
+
+ // Codex writes a file (it prints the file_change after writing, as the real CLI does), then hits its limit.
+ // Claude Code takes over and finishes: the note is the agent's own work, not an import, whichever attempt did it.
+ fs.writeFileSync(path.join(bin, 'codex'), `#!${process.execPath}
+'use strict';
+const fs = require('fs');
+const out = (o) => console.log(JSON.stringify(o));
+out({ type: 'thread.started', thread_id: 'cx-cp' });
+out({ type: 'turn.started' });
+fs.mkdirSync('notes', { recursive: true });
+fs.writeFileSync('notes/turn.txt', 'written by codex\\n');
+out({ type: 'item.completed', item: { id: 'i2', type: 'file_change', changes: [{ path: 'notes/turn.txt', kind: 'add' }], status: 'completed' } });
+out({ type: 'error', message: 'You have hit your usage limit.' });
+out({ type: 'turn.failed', error: { message: 'You have hit your usage limit.' } });
+process.exit(1);
+`, { mode: 0o755 });
+ fs.writeFileSync(path.join(bin, 'claude'), `#!${process.execPath}
+'use strict';
+const out = (o) => console.log(JSON.stringify(o));
+out({ type: 'system', subtype: 'init', session_id: 'cl-cp', model: 'fake-claude' });
+out({ type: 'assistant', message: { content: [{ type: 'text', text: 'finished the note' }] } });
+out({ type: 'result', subtype: 'success', is_error: false, result: 'finished the note', session_id: 'cl-cp' });
+`, { mode: 0o755 });
+
+ const env = {
+ ...process.env, PATH: bin, OPENVIBE_CODES_HOME: home, OPENVIBE_CODES_ALLOW_FILE_REPO: '1',
+ DEEPSEEK_API_KEY: '', OPENVIBE_AI_TOKEN: '', OPENVIBE_APP_ID: '', OPENVIBE_APP_SECRET: '',
+ };
+ const harness = createHarness({ env });
+ const lines = [];
+ let doc = null;
+ try {
+ await runJob({
+ input: JSON.stringify({ kind: 'code', repo: pathToFileURL(repo).href, prompt: 'Add the note', agent: 'codex', task_id: 'tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2T1' }),
+ harness, env, emit: (line) => lines.push(JSON.parse(line)), writeResult: (d) => { doc = d; },
+ });
+ assert.ok(doc, 'the job wrote a result');
+ // The agent hit its limit, so the job is not ok; the checkpoints of what it did are still there.
+ assert.strictEqual(doc.ok, false);
+ assert.deepStrictEqual(doc.agents, ['codex']);
+ assert.ok(doc.checkpoints, 'the result carries its checkpoints');
+ assert.match(doc.checkpoints.root, /^refs\/codes\/checkpoints\/tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2T1\/run_/);
+ assert.ok(doc.checkpoints.count >= 2, `count ${doc.checkpoints.count}`);
+ const note = doc.checkpoints.turns.flatMap((t) => t.files).find((f) => f.path === 'notes/turn.txt');
+ assert.ok(note, JSON.stringify(doc.checkpoints.turns));
+ assert.strictEqual(note.authored, true);
+ assert.ok(doc.checkpoints.turn_diff.files.some((f) => f.path === 'notes/turn.txt' && f.authored === true));
+ assert.ok(doc.checkpoints.turns.some((t) => t.files_total > 0));
+ assert.ok(!lines.some((l) => l.t === 'note' && /Checkpoints are off/.test(l.text)), 'checkpoints did not fail');
+ } finally {
+ fs.rmSync(dir, { recursive: true, force: true });
+ }
+ });
+
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });
@@ -31,7 +31,7 @@ const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
@@ -31,7 +31,7 @@ const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
await check('a manifest becomes one capability per field, and fits() reads it', () => {
await check('a manifest becomes one capability per field, and fits() reads it', () => {
assert.deepStrictEqual(runtime.capabilities(runtime.forHarness('claude-code')), [
assert.deepStrictEqual(runtime.capabilities(runtime.forHarness('claude-code')), [
- 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-none',
+ 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-git',
'harness:subagents-native', 'harness:context-handoff-session', 'harness:enforcement-native',
'harness:subagents-native', 'harness:context-handoff-session', 'harness:enforcement-native',
]);
]);
assert.deepStrictEqual(runtime.requirements({ steering: 'resume', subagents: 'native' }), ['harness:steering-resume', 'harness:subagents-native']);
assert.deepStrictEqual(runtime.requirements({ steering: 'resume', subagents: 'native' }), ['harness:steering-resume', 'harness:subagents-native']);
@@ -55,9 +55,9 @@ const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
@@ -55,9 +55,9 @@ const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
for (const id of ['codex:codex-default', 'opencode:opencode-default', 'aider:aider-default', 'deepseek:deepseek-chat']) {
for (const id of ['codex:codex-default', 'opencode:opencode-default', 'aider:aider-default', 'deepseek:deepseek-chat']) {
assert.deepStrictEqual(r.candidates.find((c) => c.id === id), { id, included: false, reason: 'lacks harness:subagents-native' });
assert.deepStrictEqual(r.candidates.find((c) => c.id === id), { id, included: false, reason: 'lacks harness:subagents-native' });
}
}
- const none = route({ task: 'edit', extra: { capabilities: ['harness:checkpointing-git'] }, harnesses, now: Date.now() });
- assert.strictEqual(none.selected, null, 'no harness declares checkpointing yet');
- assert.ok(none.candidates.every((c) => c.reason === 'lacks harness:checkpointing-git'));
+ const git = route({ task: 'edit', extra: { capabilities: ['harness:checkpointing-git'] }, harnesses, now: Date.now() });
+ assert.ok(git.selected, 'checkpointing is declared by every catalog harness');
+ assert.ok(git.candidates.every((c) => c.included), 'a job can checkpoint any harness');
});
});
await check('the tier router skips a ladder rung its manifest cannot meet', () => {
await check('the tier router skips a ladder rung its manifest cannot meet', () => {
@@ -41,7 +41,7 @@ function priced(row, freshUsd) {
@@ -41,7 +41,7 @@ function priced(row, freshUsd) {
region: 'global',
region: 'global',
trust: 'partner',
trust: 'partner',
capabilities: ['task:edit', 'harness:host-access', 'harness:mcp', 'harness:resume', 'harness:edit', 'harness:tools', 'runtime:code',
capabilities: ['task:edit', 'harness:host-access', 'harness:mcp', 'harness:resume', 'harness:edit', 'harness:tools', 'runtime:code',
- 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-none',
+ 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-git',
'harness:subagents-none', 'harness:context-handoff-session', 'harness:enforcement-client-boundary'],
'harness:subagents-none', 'harness:context-handoff-session', 'harness:enforcement-client-boundary'],
capacity: { workers: { harness: 2 } },
capacity: { workers: { harness: 2 } },
health: { status: 'degraded' },
health: { status: 'degraded' },
@@ -55,7 +55,7 @@ function priced(row, freshUsd) {
@@ -55,7 +55,7 @@ function priced(row, freshUsd) {
const offer = toOffer(deepseek, deepseek.agents[0]);
const offer = toOffer(deepseek, deepseek.agents[0]);
assert.strictEqual(offer.offer_id, 'deepseek:deepseek-chat');
assert.strictEqual(offer.offer_id, 'deepseek:deepseek-chat');
assert.deepStrictEqual(offer.capabilities, ['task:edit', 'task:review', 'harness:host-access', 'harness:resume', 'harness:edit', 'harness:review', 'harness:tools',
assert.deepStrictEqual(offer.capabilities, ['task:edit', 'task:review', 'harness:host-access', 'harness:resume', 'harness:edit', 'harness:review', 'harness:tools',
- 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-none',
+ 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-git',
'harness:subagents-none', 'harness:context-handoff-session', 'harness:enforcement-client-boundary']);
'harness:subagents-none', 'harness:context-handoff-session', 'harness:enforcement-client-boundary']);
assert.strictEqual(offer.trust, 'external');
assert.strictEqual(offer.trust, 'external');
assert.deepStrictEqual(offer.health, { status: 'up' });
assert.deepStrictEqual(offer.health, { status: 'up' });
@@ -78,7 +78,7 @@ function priced(row, freshUsd) {
@@ -78,7 +78,7 @@ function priced(row, freshUsd) {
const browsing = { ...rows[0], task_capabilities: ['edit', 'browse'] };
const browsing = { ...rows[0], task_capabilities: ['edit', 'browse'] };
const offer = toOffer(browsing, browsing.agents[0], { now });
const offer = toOffer(browsing, browsing.agents[0], { now });
assert.deepStrictEqual(offer.capabilities, ['task:edit', 'task:browse', 'harness:host-access', 'harness:mcp', 'harness:long-autonomy', 'harness:resume', 'harness:edit', 'harness:review', 'harness:tools', 'harness:vision', 'runtime:code',
assert.deepStrictEqual(offer.capabilities, ['task:edit', 'task:browse', 'harness:host-access', 'harness:mcp', 'harness:long-autonomy', 'harness:resume', 'harness:edit', 'harness:review', 'harness:tools', 'harness:vision', 'runtime:code',
- 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-none',
+ 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-git',
'harness:subagents-native', 'harness:context-handoff-session', 'harness:enforcement-native']);
'harness:subagents-native', 'harness:context-handoff-session', 'harness:enforcement-native']);
assert.ok(contracts.validate('platform.resource-offer@1', JSON.parse(JSON.stringify(offer))).valid);
assert.ok(contracts.validate('platform.resource-offer@1', JSON.parse(JSON.stringify(offer))).valid);
});
});
deleted file mode 100644
deleted file mode 100644
@@ -1,77 +0,0 @@
@@ -1,77 +0,0 @@
-'use strict';
-/**
- * The runtime capability manifest (engine/runtime.js): every catalog harness declares what it can do, every offer
- * carries it as `harness:<field>-<value>` capabilities, and both routers (placement and the tier router) exclude a
- * harness that does not declare what the task needs — no provider-name conditional in either.
- */
-const assert = require('assert');
-const fs = require('fs');
-const path = require('path');
-const { createHarnesses } = require('../server/domain/harnesses');
-const { toOffer, route } = require('../server/domain/harness-placement');
-const runtime = require('../engine/runtime');
-const { pick } = require('../engine/router');
-const { check, done } = require('./helpers/boot');
-
-const catalogPath = path.join(__dirname, '..', 'server', 'data', 'harness-offers.json');
-const rows = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
-
-(async () => {
- await check('every catalog harness declares every field, with a known value', () => {
- for (const row of rows) {
- const manifest = runtime.forHarness(row.id);
- assert.ok(manifest, `${row.id} has no runtime manifest`);
- assert.deepStrictEqual(Object.keys(manifest).sort(), Object.keys(runtime.FIELDS).sort(), row.id);
- for (const [field, values] of Object.entries(runtime.FIELDS)) {
- assert.ok(values.includes(manifest[field]), `${row.id}.${field}: ${manifest[field]} is not one of ${values.join('|')}`);
- }
- }
- assert.deepStrictEqual(Object.keys(runtime.MANIFEST).sort(), rows.map((row) => row.id).sort(), 'no manifest without a catalog row');
- });
-
- await check('a manifest becomes one capability per field, and fits() reads it', () => {
- assert.deepStrictEqual(runtime.capabilities(runtime.forHarness('claude-code')), [
- 'harness:terminal-status-structured', 'harness:steering-resume', 'harness:checkpointing-none',
- 'harness:subagents-native', 'harness:context-handoff-session', 'harness:enforcement-native',
- ]);
- assert.deepStrictEqual(runtime.requirements({ steering: 'resume', subagents: 'native' }), ['harness:steering-resume', 'harness:subagents-native']);
- assert.strictEqual(runtime.fits('claude-code', { subagents: 'native' }), true);
- assert.strictEqual(runtime.fits('codex', { subagents: 'native' }), false);
- assert.strictEqual(runtime.fits('cx', ['harness:steering-resume']), true, 'the portal runner id names the same harness');
- assert.strictEqual(runtime.fits('pg', ['harness:steering-resume']), false, 'an unknown runner declares nothing');
- assert.strictEqual(runtime.fits('pg', []), true, 'nothing required, nothing to exclude');
- });
-
- await check('every offer carries the manifest as capabilities the placement router can require', () => {
- const harnesses = createHarnesses({ catalogPath });
- for (const harness of harnesses.list()) {
- const offer = toOffer(harness, harnesses.agents(harness.id)[0]);
- for (const cap of runtime.capabilities(runtime.forHarness(harness.id))) {
- assert.ok(offer.capabilities.includes(cap), `${offer.offer_id} lacks ${cap}`);
- }
- }
- const r = route({ task: 'edit', extra: { capabilities: ['harness:subagents-native'] }, harnesses, now: Date.now() });
- assert.strictEqual(r.selected.split(':')[0], 'claude-code');
- for (const id of ['codex:codex-default', 'opencode:opencode-default', 'aider:aider-default', 'deepseek:deepseek-chat']) {
- assert.deepStrictEqual(r.candidates.find((c) => c.id === id), { id, included: false, reason: 'lacks harness:subagents-native' });
- }
- const none = route({ task: 'edit', extra: { capabilities: ['harness:checkpointing-git'] }, harnesses, now: Date.now() });
- assert.strictEqual(none.selected, null, 'no harness declares checkpointing yet');
- assert.ok(none.candidates.every((c) => c.reason === 'lacks harness:checkpointing-git'));
- });
-
- await check('the tier router skips a ladder rung its manifest cannot meet', () => {
- const ctx = {
- cfg: { subagents: { mm: { enabled: true, max: 2 }, cm: { enabled: true, max: 2 } } },
- health: { mm: { state: 'ok' }, cm: { state: 'ok' } },
- counts: {}, models: {}, runs: [],
- ladder: { basic: [['mm', null, 'high'], ['cm', null, 'high']] },
- };
- assert.strictEqual(pick('Fix the parser', ctx).runner, 'mm', 'no requirement: the first rung stays');
- assert.strictEqual(pick('Fix the parser', { ...ctx, needs: ['harness:subagents-native'] }).runner, 'cm');
- assert.strictEqual(pick('Fix the parser', { ...ctx, needs: { steering: 'resume' } }).runner, 'mm');
- assert.strictEqual(pick('Fix the parser', { ...ctx, needs: ['harness:subagents-native'], ladder: { basic: [['mm', null, 'high']] } }), null);
- });
-
- done();
-})().catch((e) => { console.error(e); process.exit(1); });