openvibers/openvibe.codes Commits
Proof, not promises: a finished task shows its tests, its pages, its diff and a second opinion (C8) (#170)
Changes
@@@ -31,7 +31,8 @@"Improve OpenVibe (/improve): a guided run from repository choice to a task on your machine, plus the contributor path","The community documents (code of conduct, contributing, contributor ladder, moderation) rendered from this repository","Permanent redirects for every former console address (301 for reads, 308 for writes) to openvibe.services",-- "robots.txt, sitemap.xml, llms.txt and llms-full.txt; per-actor and per-address limits; truthful readiness"++ "robots.txt, sitemap.xml, llms.txt and llms-full.txt; per-actor and per-address limits; truthful readiness",++ "Proof of a finished task (engine/proof.js, engine/review.js): the repo's own tests, the pages the diff touches (phone and desktop, headless Chrome), the diff, and a second model's review of another family; kept in codes_task_proof, one event per item, and the task's outcome in plain words"],"notYet": ["A hosted runner (tasks on OpenVibe.Run workers for people who do not run agents themselves) with budgets and metering",@@@ -51,7 +52,8 @@"community documents","redirects for the moved developer console","run a coding task on your own machine from any browser (/run; openvibe-codes job through OpenVibe Run and the person's own OpenVibe Node)",-- "user JSON API for signed-in people to list, start, read and cancel their own tasks"++ "user JSON API for signed-in people to list, start, read and cancel their own tasks",++ "proof of a finished task: its tests, screenshots, diff and a second-model review, and the outcome in plain words"],"updated": "2026-10-30"}@@@ -10,6 +10,7 @@ const path = require('path');const { createHarness } = require('../harness');const { runJob, resultWriter, jobEnv } = require('../harness/job');const { task } = require('./task');++const { createProofAdapters } = require('../harness/proof');const { version: VERSION } = require('../package.json');const { probe: probeLogins } = require('../engine/logins');@@@ -164,7 +165,16 @@ async function main() {let wanted = {};try { wanted = jobEnv(JSON.parse(input)); } catch { wanted = {}; }const jobHarness = createHarness({ env: { ...process.env, ...wanted } });-- await runJob({ input, harness: jobHarness, emit: out, writeResult: resultWriter(out) });++ // The proof adapters: the repo's own tests, its pages in a browser, its diff, and the second model. The review++ // spends the same run token the attempt already carries, so it names the same person.++ const proof = createProofAdapters({++ cwd: process.cwd(),++ env: { ...process.env, ...wanted },++ jobEnv: wanted,++ // The models this machine can offer a reviewer: the catalog's own.++ models: (jobHarness.agents() || []).flatMap((a) => (a.agents || []).map((x) => x.model)).filter(Boolean),++ });++ await runJob({ input, harness: jobHarness, emit: out, writeResult: resultWriter(out), proof });return 0;}if (cmd === 'runs') {@@@ -17,6 +17,8 @@* engine.joblines the lines a task's page shows while it runs* engine.logins what this machine has: each CLI, signed in or not, and its limit windows* engine.redact the secrets that never reach an event stream++ * engine.proof the proof of a finished attempt: its tests, its pages, its diff, and a second opinion++ * engine.review the second opinion itself: a model of another family, through OpenVibe.AI** The modules do not depend on each other in a cycle, and none of them reads the portal's log folder.*/@@@ -35,6 +37,8 @@ const repomap = require('./repomap');const joblines = require('./joblines');const logins = require('./logins');const redact = require('./redact');++const proof = require('./proof');++const review = require('./review');module.exports = {router,@@@ -51,6 +55,8 @@ module.exports = {joblines,logins,redact,++ proof,++ review,// The names a caller usually wants, in one place.classify: router.classify,pick: router.pick,nnew file mode 100644@@@ -0,0 +1,297 @@++'use strict';++++/**++ * Proof, not promises: what a finished attempt has to show.++ *++ * When an attempt says it is done, this asks for the evidence: the tests the change reaches, the pages it touched++ * (phone and desktop), the diff, and a second model's opinion. Everything it touches is an injected adapter, so the++ * order is the same in a test with fakes as on a machine, and a step that cannot run says so instead of being++ * quietly left out.++ *++ * collectProof({ cwd, files, adapters, author, emit }) the proof: its items, and the outcome in plain words++ * testPlan({ pkg, files, env }) the repo's own test command, and why it is that one++ * pageFiles(files) the changed files that are pages++ * routesOf(pages) the routes those pages are served at++ * outcomeOf(items) 'Done with proof' / "Didn't finish, with what is left"++ *++ * The four steps, in order:++ *++ * tests the repo's own runner, narrowed to the tests the change reaches. The whole suite runs only when++ * the change touches what every test shares and the owner's guard allows it (CODES_PROOF_FULL_SUITE=1):++ * a machine may not spend an hour of its owner's battery on every task.++ * screenshots phone (390) and desktop (1440) of the pages the diff touches, because a page that overflows is a++ * bug the diff alone never shows.++ * diff the stats: what changed, how many lines, which files.++ * review a model of another family, through OpenVibe.AI. A review by the author's own family is refused:++ * that would be a second reading of the same mind, not a second opinion.++ *++ * The outcome is the three plain words a person reads: Done with proof, Didn't finish (with what is left), or++ * Stopped by you.++ */++++const { pickReviewer, authorFamily: familyOfAuthor } = require('./review');++++/** The changed files that are pages: the site's own files, and anything served as HTML or CSS. */++const PAGE_FILE = /^(?:public\/|server\/render\/|server\/http\/)|\.(?:html?|css)$/i;++++/** The files that make every test matter: the shared ones. A change here runs the whole suite (when allowed). */++const SHARED_FILE = /^(?:package\.json|package-lock\.json|engine\/index\.js|server\/app\.js|server\/db\.js|server\/config\.js)$/;++++/** The kinds of proof item, in the order they are collected. */++const KINDS = ['tests', 'screenshots', 'diff', 'review'];++++/** The plain words for a finished task. */++const OUTCOME = {++ done: 'Done with proof',++ unfinished: "Didn't finish",++ stopped: 'Stopped by you',++};++++/**++ * The changed files that are pages.++ *++ * @param {string[]} files the changed files++ * @returns {string[]} the pages among them++ */++function pageFiles(files) {++ return (Array.isArray(files) ? files : []).filter((f) => typeof f === 'string' && PAGE_FILE.test(f));++}++++/**++ * The words the repo's own test runner filters by, for the changed files: a test file is named after what it++ * tests ('test/engine-proof.test.js' for 'engine/proof.js'), so the name without its extension finds it.++ *++ * @param {string[]} files the changed files++ * @returns {string[]} the filter words, unique, without the words every file shares++ */++function testWords(files) {++ const skip = new Set(['index', 'main', 'app', 'server', 'public', 'src', 'lib', 'test', 'tests', 'util', 'utils', 'helper', 'helpers', 'config']);++ const words = new Set();++ for (const f of Array.isArray(files) ? files : []) {++ if (typeof f !== 'string') continue;++ const name = String(f).split('/').pop().replace(/\.[^.]+$/, '');++ for (const part of name.split(/[-_.\s]+/)) {++ const w = part.toLowerCase();++ if (w.length >= 3 && !skip.has(w)) words.add(w);++ }++ }++ return [...words].slice(0, 8);++}++++/**++ * The file a `node <file>` test script runs, or null.++ *++ * 'npm test' would run it with one more process in the way, and a machine's npm may be a guard that cannot tell a++ * narrowed run from a whole one. Running the script itself is the same runner, with the words the plan chose.++ */++function nodeTestScript(script) {++ const m = /^node\s+([^\s-][^\s]*)$/.exec(String(script || '').trim());++ return m ? m[1] : null;++}++++/**++ * The repo's own test command, and how narrow it is.++ *++ * The runner is the repo's: 'npm test' when package.json has it (or the file it runs, when that is `node <file>`),++ * then pytest, go and cargo. The suite is narrowed to the tests the change reaches; the whole suite runs only when++ * the change touches what every test shares and the owner's guard allows it (CODES_PROOF_FULL_SUITE=1).++ *++ * @param {object} what++ * @param {object} what.pkg the repo's package.json (or null)++ * @param {string[]} what.files the changed files++ * @param {object} what.env the environment++ * @returns {{ command: string, args: string[], mode: string, reason: string }} the plan++ */++function testPlan({ pkg = null, files = [], env = {} } = {}) {++ const scripts = pkg && pkg.scripts ? pkg.scripts : {};++ const test = typeof scripts.test === 'string' ? scripts.test : null;++ const changed = Array.isArray(files) ? files : [];++ const shared = changed.some((f) => SHARED_FILE.test(String(f || '')));++ // The guard: the whole suite runs only when the owner allows it. CODES_PROOF_FULL_SUITE is the proof step's own++ // name for it; OV_ALLOW_SUITE is the fleet's (the harness refuses a whole suite without it too).++ const allowFull = /^(1|true|yes)$/i.test(String(env.CODES_PROOF_FULL_SUITE || env.OV_ALLOW_SUITE || ''));++ const words = testWords(changed);++++ if (test) {++ const direct = nodeTestScript(test);++ const base = direct ? { command: 'node', args: [direct] } : { command: 'npm', args: ['test'] };++ const narrowed = direct ? [direct, ...words] : ['test', '--', ...words];++ if (shared && allowFull) return { ...base, mode: 'full', reason: 'the change touches what every test shares, and the full suite is allowed' };++ if (!shared) {++ if (!words.length) return { ...base, mode: 'targeted', reason: 'no test name matches the changed files, so the suite runs as it is' };++ return { command: base.command, args: narrowed, mode: 'targeted', reason: 'only the tests the change reaches (' + words.join(', ') + ')' };++ }++ return { command: base.command, args: narrowed, mode: 'targeted', reason: 'the change touches what every test shares, but the full suite is not allowed (CODES_PROOF_FULL_SUITE=1)' };++ }++ if (pkg && (pkg.go || (pkg.dependencies && pkg.dependencies.go))) return { command: 'go', args: ['test', './...'], mode: 'full', reason: "the repo's own Go suite" };++ if (pkg && (pkg.pytest || (pkg.dependencies && pkg.dependencies.pytest))) return { command: 'pytest', args: [], mode: 'full', reason: "the repo's own Python suite" };++ if (pkg && pkg.rust) return { command: 'cargo', args: ['test'], mode: 'full', reason: "the repo's own Rust suite" };++ return { command: '', args: [], mode: 'none', reason: 'the repo has no test command of its own' };++}++++/**++ * The outcome in plain words: done with proof, or what is left.++ *++ * @param {object[]} items the proof items++ * @returns {{ state: string, note: string }} the outcome++ */++function proofNote(items) {++ const list = Array.isArray(items) ? items : [];++ const failed = list.filter((i) => i && i.ok === false);++ const passed = list.filter((i) => i && i.ok === true && i.skipped !== true);++ const skipped = list.filter((i) => i && i.skipped === true);++ if (failed.length) {++ const left = failed.map((i) => (i.label || 'a step') + ': ' + (i.summary || 'it failed')).join('; ');++ return "Didn't finish, with what is left: " + left + '.';++ }++ const skippedText = skipped.length++ ? ' (' + skipped.map((i) => (i.label || 'a step') + ' was skipped: ' + (i.summary || 'not available here')).join('; ') + ')'++ : '';++ if (!passed.length) return 'Done.' + skippedText;++ return 'Done with proof: ' + passed.map((i) => i.label).join(', ') + '.' + skippedText;++}++++function outcomeOf(items) {++ const list = Array.isArray(items) ? items : [];++ const failed = list.filter((i) => i && i.ok === false);++ return { state: failed.length ? 'unfinished' : 'done', note: proofNote(list) };++}++++/**++ * The routes the changed pages are served at: a public/index.html is the home, a public/a/b.html is /a/b.++ *++ * @param {string[]} pages the changed page files++ * @returns {string[]} the routes++ */++function routesOf(pages) {++ const out = [];++ for (const p of Array.isArray(pages) ? pages : []) {++ if (!String(p).startsWith('public/')) continue;++ let route = String(p).slice('public/'.length).replace(/\.(html?|css)$/i, '');++ route = route.replace(/^(.*\/)?index$/i, ''); // public/index.html is the home, public/a/index.html is /a++ if (route) out.push('/' + route);++ else if (/^public\/index\.html?$/i.test(String(p))) out.push('/');++ }++ return [...new Set(out.length ? out : ['/'])];++}++++/**++ * Collect the proof of one finished attempt.++ *++ * @param {object} what++ * @param {string} what.cwd the repo the change is in++ * @param {string[]} [what.files] the changed files++ * @param {object} what.adapters { run, diff, screenshot, review, models, env }++ * @param {object} [what.author] { agent, model } the author of the change++ * @param {function} [what.emit] called with every item as it is collected++ * @returns {Promise<{ items: object[], outcome: object, review: object|null }>} the proof++ */++async function collectProof({ cwd, files = [], adapters = {}, author = null, emit = null } = {}) {++ if (!cwd) throw new TypeError('proof needs the repo it is about');++ const run = adapters.run;++ if (typeof run !== 'function') throw new TypeError('proof needs a run adapter');++ const items = [];++ const say = async (item) => { items.push(item); if (typeof emit === 'function') await emit(item); };++++ // The diff: what changed, in numbers and in words. Every later step reads it.++ const diff = typeof adapters.diff === 'function' ? await adapters.diff({ cwd }) : null;++ const changed = Array.isArray(diff && diff.files) ? diff.files : (Array.isArray(files) ? files : []);++ const stats = diff && diff.stats ? String(diff.stats) : '';++ const patch = diff && diff.patch ? String(diff.patch) : '';++++ // 1. the tests the change reaches.++ let pkg = null;++ const pkgRead = await run(['node', '-e', "try{process.stdout.write(require('fs').readFileSync('package.json','utf8'))}catch(e){}"], { cwd });++ if (pkgRead && pkgRead.code === 0 && pkgRead.output) { try { pkg = JSON.parse(pkgRead.output); } catch { pkg = null; } }++ const plan = testPlan({ pkg, files: changed, env: adapters.env || {} });++ if (!plan.command) {++ await say({ kind: 'tests', label: 'tests', ok: true, skipped: true, summary: plan.reason, mode: 'none' });++ } else {++ const result = await run([plan.command, ...plan.args], { cwd, timeoutMs: adapters.testTimeoutMs || 600000 });++ const output = String((result && result.output) || '');++ const ok = !!result && result.code === 0;++ await say({++ kind: 'tests', label: 'tests', ok, skipped: false, mode: plan.mode, reason: plan.reason,++ command: [plan.command, ...plan.args].join(' '),++ summary: ok ? plan.reason : (output.trim().split('\n').slice(-6).join(' ') || 'the tests failed (exit ' + result.code + ')'),++ });++ }++++ // 2. the pages it touched, at the widths a person reads them.++ const pages = pageFiles(changed);++ if (!pages.length) {++ await say({ kind: 'screenshots', label: 'screenshots', ok: true, skipped: true, summary: 'the change touches no page' });++ } else if (typeof adapters.screenshot !== 'function') {++ await say({ kind: 'screenshots', label: 'screenshots', ok: true, skipped: true, summary: 'this machine cannot take screenshots' });++ } else {++ const shots = [];++ for (const width of [390, 1440]) {++ const shot = await adapters.screenshot({ cwd, width, files: pages, routes: routesOf(pages), pkg });++ shots.push({ width, ...(shot || {}) });++ }++ const made = shots.filter((s) => s && !s.skipped);++ const bad = made.filter((s) => s.ok === false);++ // A machine with nothing to shoot with (no browser, a site that did not start) says so: it is a skip, and the++ // reason is the adapter's own words, not a clean bill of health.++ const why = shots.find((s) => s && s.skipped);++ await say({++ kind: 'screenshots', label: 'screenshots', ok: !bad.length, skipped: !made.length,++ summary: bad.length++ ? bad.map((s) => s.width + 'px: ' + (s.summary || 'the page did not render')).join('; ')++ : (made.length ? made.length + ' screenshots of ' + pages.length + ' page' + (pages.length === 1 ? '' : 's')++ : (why && why.summary) || 'no page was there to shoot'),++ shots,++ });++ }++++ // 3. the diff itself.++ await say({++ kind: 'diff', label: 'diff', ok: true, skipped: false,++ summary: stats || (changed.length ? changed.length + ' file' + (changed.length === 1 ? '' : 's') + ' changed' : 'nothing changed'),++ files: changed, stats, patch: patch.slice(0, 262144),++ });++++ // 4. the second opinion, from a model of another family.++ const family = familyOfAuthor(author || {});++ let verdict = null;++ if (typeof adapters.review !== 'function') {++ await say({ kind: 'review', label: 'review', ok: true, skipped: true, summary: 'no reviewer is connected here' });++ } else {++ const models = Array.isArray(adapters.models) ? adapters.models : [];++ const chosen = pickReviewer({ models, author: family });++ if (!chosen) {++ // Nobody reviewed this: not a refusal, and not a second opinion either. It is said out loud as a skip.++ await say({ kind: 'review', label: 'review', ok: true, skipped: true, summary: 'no model of another family is connected, so no second opinion was asked for' });++ } else {++ try {++ const tests = items.find((i) => i.kind === 'tests');++ verdict = await adapters.review({++ patch, summary: (diff && diff.summary) || '',++ tests: tests ? tests.summary : null,++ model: chosen.model, family: chosen.family,++ });++ } catch (e) {++ // A reviewer that is not connected is a skip, not a failure: the change was not refused, nobody looked.++ if (e && (e.code === 'review.no_token' || e.code === 'review.no_model')) {++ await say({ kind: 'review', label: 'review', ok: true, skipped: true, summary: e.message });++ return { items, outcome: outcomeOf(items), review: null };++ }++ verdict = { family: chosen.family, model: chosen.model, verdict: 'changes_requested', summary: 'the review failed: ' + e.message, error: true };++ }++ if (!verdict || !verdict.family || verdict.family === family) {++ await say({ kind: 'review', label: 'review', ok: false, skipped: false, family: verdict && verdict.family, summary: "the review came from the author's own family, so it is not a second opinion" });++ verdict = null;++ } else {++ await say({++ kind: 'review', label: 'review', ok: verdict.verdict === 'approved', skipped: false,++ family: verdict.family, model: verdict.model,++ summary: verdict.summary || (verdict.verdict === 'approved' ? 'approved' : 'changes requested'),++ });++ }++ }++ }++++ return { items, outcome: outcomeOf(items), review: verdict };++}++++module.exports = { collectProof, testPlan, testWords, pageFiles, routesOf, outcomeOf, proofNote, OUTCOME, KINDS, PAGE_FILE, SHARED_FILE };nnew file mode 100644@@@ -0,0 +1,171 @@++'use strict';++++/**++ * The second opinion on a change: a model of another family than the one that wrote it.++ *++ * "Another family" is the rule that makes the second opinion worth reading. A model of the same family tends to++ * repeat its own reasoning, so the family is part of the verdict: a review by the author's own family is not++ * accepted, and the collector says so instead of pretending a second look happened.++ *++ * The call goes to OpenVibe.AI, the one place that holds the person's keys (stage C3): POST /v1/chat/completions with++ * the attempt's run token, or /v1/messages for a provider that speaks that dialect. The token is passed in, never read++ * from here: this module never sees a secret and never logs one.++ *++ * familyOf(model) the family a model belongs to ('anthropic', 'openai', 'deepseek', …)++ * authorFamily({ agent, model }) the family that wrote the change++ * pickReviewer({ models, author }) a model of another family, or null when there is none++ * reviewWithAi({ … }) the review as { family, verdict, summary }++ *++ * The verdict is 'approved' or 'changes_requested'. The model is asked for JSON; a model that answers in prose is++ * read by its words ('approved', 'looks good' → approved; 'changes', 'needs work' → changes_requested), because a++ * review that cannot be read is not a review.++ */++++/** The families this knows, by the model's own name. */++const FAMILIES = [++ { family: 'anthropic', re: /claude/i },++ { family: 'openai', re: /\bgpt-|\bo[134]\b|codex/i },++ { family: 'google', re: /gemini/i },++ { family: 'deepseek', re: /deepseek/i },++ { family: 'minimax', re: /minimax|abab/i },++ { family: 'qwen', re: /qwen/i },++ { family: 'moonshot', re: /kimi|moonshot/i },++];++++/** The families Codes' own harnesses belong to, by the harness id. */++const HARNESS_FAMILY = {++ 'claude-code': 'anthropic',++ codex: 'openai',++ 'command-code': 'deepseek',++ opencode: 'qwen',++ deepseek: 'deepseek',++ aider: 'openai',++ 'openvibe-ai': 'openai',++ 'openai-compatible': 'openai',++};++++/**++ * The family a model belongs to, or null when its name says nothing.++ *++ * @param {string} model the model's name++ * @returns {string|null} the family++ */++function familyOf(model) {++ const name = String(model || '').trim();++ if (!name) return null;++ for (const f of FAMILIES) if (f.re.test(name)) return f.family;++ return null;++}++++/**++ * The family that wrote the change: the model's own name first, the harness second.++ *++ * @param {object} what { agent, model }++ * @returns {string|null} the family++ */++function authorFamily({ agent = null, model = null } = {}) {++ return familyOf(model) || HARNESS_FAMILY[String(agent || '')] || null;++}++++/**++ * A model of another family, for the second opinion. The first one that is not the author's family wins.++ *++ * @param {object} what { models, author }++ * @returns {{ model: string, family: string }|null} the reviewer, or null when every model is the author's family++ */++function pickReviewer({ models = [], author = null } = {}) {++ const list = Array.isArray(models) ? models : [];++ for (const m of list) {++ const name = typeof m === 'string' ? m : (m && m.model) || null;++ if (!name) continue;++ const family = familyOf(name);++ if (!family || (author && family === author)) continue;++ return { model: name, family };++ }++ return null;++}++++/** The prompt the reviewer gets: the change, the tests it claims to pass, and what is being asked of it. */++function reviewPrompt({ patch = '', summary = '', tests = null } = {}) {++ return [++ 'You are reviewing a change to a repository. Answer only with JSON:',++ '{"verdict":"approved"|"changes_requested","summary":"one paragraph, what you checked and what you found"}.',++ 'Answer "approved" when the change does what it says and holds together; "changes_requested" when it is wrong,',++ 'unsafe, or leaves the work unfinished. Be concrete: name the file and the line.',++ '',++ summary ? 'What the change was asked to do:\n' + String(summary).slice(0, 4000) : '',++ tests ? 'The tests the author says pass:\n' + String(tests).slice(0, 2000) : '',++ '',++ 'The diff:\n' + String(patch || '').slice(0, 120000),++ ].filter(Boolean).join('\n');++}++++/** The answer as JSON, or as the words in it. */++function readVerdict(text) {++ const raw = String(text || '').trim();++ if (!raw) return { verdict: 'changes_requested', summary: 'the reviewer said nothing' };++ const start = raw.indexOf('{');++ const end = raw.lastIndexOf('}');++ if (start >= 0 && end > start) {++ try {++ const doc = JSON.parse(raw.slice(start, end + 1));++ const verdict = String(doc.verdict || '').toLowerCase();++ if (/approv|looks good|lgtm/.test(verdict)) return { verdict: 'approved', summary: String(doc.summary || raw).slice(0, 4000) };++ if (/change|reject|not good/.test(verdict)) return { verdict: 'changes_requested', summary: String(doc.summary || raw).slice(0, 4000) };++ if (doc.summary) return { verdict: 'changes_requested', summary: String(doc.summary).slice(0, 4000) };++ } catch { /* not JSON: read the words */ }++ }++ if (/\bapprov|looks good|looks fine|lgtm|no changes\b/i.test(raw)) return { verdict: 'approved', summary: raw.slice(0, 4000) };++ return { verdict: 'changes_requested', summary: raw.slice(0, 4000) };++}++++/**++ * The review itself: one request to OpenVibe.AI, read as a verdict.++ *++ * @param {object} what++ * @param {string} what.baseUrl OpenVibe.AI's OpenAI-compatible root++ * @param {string} what.token the task's token (never logged)++ * @param {string} what.model the reviewer's model++ * @param {string} what.family the reviewer's family++ * @param {string} [what.patch] the diff++ * @param {string} [what.summary] what the change was for++ * @param {function} [what.fetchImpl] fetch, for a test++ * @returns {Promise<{ family: string, verdict: string, summary: string }>} the verdict++ */++async function reviewWithAi({++ baseUrl = 'https://ai.openvibe.services/v1',++ token,++ model,++ family = null,++ patch = '',++ summary = '',++ tests = null,++ fetchImpl = globalThis.fetch,++} = {}) {++ if (!token) throw new Error('the review needs the task\'s token');++ if (!model) throw new Error('the review needs a model');++ const root = String(baseUrl).replace(/\/+$/, '');++ const prompt = reviewPrompt({ patch, summary, tests });++ const headers = { 'content-type': 'application/json', authorization: `Bearer ${token}` };++ let res = await fetchImpl(`${root}/chat/completions`, {++ method: 'POST', headers, body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }] }),++ });++ if (res && res.status === 404) {++ // A provider that speaks the messages dialect instead.++ res = await fetchImpl(`${root}/messages`, {++ method: 'POST', headers, body: JSON.stringify({ model, max_tokens: 1200, messages: [{ role: 'user', content: prompt }] }),++ });++ }++ if (!res || !res.ok) throw new Error(`OpenVibe.AI refused the review (HTTP ${res && res.status})`);++ const body = await res.json().catch(() => null);++ const text = body && body.choices && body.choices[0] && body.choices[0].message && body.choices[0].message.content++ ? body.choices[0].message.content++ : (body && body.content && Array.isArray(body.content) ? body.content.map((c) => c && c.text).filter(Boolean).join(' ') : '');++ const verdict = readVerdict(text);++ return { family: family || familyOf(model), model, verdict: verdict.verdict, summary: verdict.summary };++}++++module.exports = {++ FAMILIES, HARNESS_FAMILY,++ familyOf, authorFamily, pickReviewer, reviewPrompt, readVerdict, reviewWithAi,++};@@@ -17,6 +17,7 @@ const fs = require('fs');const os = require('os');const path = require('path');const { execFile } = require('child_process');++const { collectProof } = require('../engine/proof');const JOB_PROTOCOL = '1.0.0'; // the job's contract with the Node (stdin, stdout lines, fd 3): unchanged by 1.1's linesconst LINES = '1.1'; // 1.1: lines carry step ids, inputs, results, progress, helpers; the result keeps `log`@@@ -276,7 +277,7 @@ async function pushBranch(dir, job, { env, fetchImpl, base }) {* Run one job. `harness` is createHarness(); `emit(line)` prints a line; `writeResult(doc)` hands the result over.* → the result document.*/--async function runJob({ input, harness, emit, writeResult, env = process.env, tmp = os.tmpdir(), fetchImpl = globalThis.fetch }) {++async function runJob({ input, harness, emit, writeResult, env = process.env, tmp = os.tmpdir(), fetchImpl = globalThis.fetch, proof = null }) {const started = Date.now();const log = [];const say = (doc) => { if (doc && doc.t && doc.t !== 'result_doc') log.push(doc); emit(JSON.stringify(doc)); };@@@ -317,6 +318,24 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tmresult.cost_usd = last && last.total_cost_usd ? last.total_cost_usd : 0;Object.assign(result, await changes(work));say({ t: 'step', text: result.files.length ? `${result.files.length} file${result.files.length === 1 ? '' : 's'} changed` : '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.++ if (proof) {++ say({ t: 'step', text: 'Collecting the proof' });++ try {++ const collected = await collectProof({++ cwd: work,++ files: result.files.map((f) => f.path),++ adapters: proof,++ author: { agent: job.agent, model: job.model },++ emit: (item) => say({ t: 'proof', kind: item.kind, label: item.label, ok: item.ok, skipped: !!item.skipped, summary: item.summary, family: item.family || null }),++ });++ result.proof = { items: collected.items.map((i) => ({ kind: i.kind, label: i.label, ok: i.ok, skipped: !!i.skipped, summary: i.summary, family: i.family || null, detail: i.shots ? { shots: i.shots } : (i.kind === 'diff' ? { files: i.files, stats: i.stats } : null) })), outcome: collected.outcome };++ } catch (e) {++ say({ t: 'note', level: 'warning', text: `The proof could not be collected: ${scrub(e.message)}` });++ result.proof = { items: [], outcome: null };++ }++ }if (job.push && result.ok && result.files.length) {say({ t: 'step', text: 'Pushing a new branch' });try {nnew file mode 100644@@@ -0,0 +1,323 @@++'use strict';++++/**++ * The proof adapters on a machine: the repo's own tests, its pages in a browser, its diff, and the second model.++ *++ * The engine (engine/proof.js) says what to do; this file does it. Every effect is a separate adapter so a test can++ * replace one of them: the tests run through the repo's own runner, the pages are shot with headless Chrome at a++ * phone's and a desktop's width, and the review goes to OpenVibe.AI.++ *++ * createProofAdapters({ cwd, env, fetchImpl, chromePath, playwrightPath })++ * run(argv, { cwd }) a command in the repo, with its output++ * diff() the diff and its stats, against HEAD++ * screenshot({ width, … }) one page at one width, with the overflow and console errors++ * review({ patch, … }) the second opinion (OpenVibe.AI)++ * models the models a reviewer may be chosen from++ *++ * The review is behind an interface (engine/review.js). The token that names the person is the run token the attempt++ * already carries (stage C3): it comes in the job's own input, is never read from the machine's other keys, and when++ * there is none the adapter says so instead of pretending a second opinion happened.++ */++++const fs = require('fs');++const net = require('net');++const path = require('path');++const { execFile, spawn } = require('child_process');++const { reviewWithAi, familyOf } = require('../engine/review');++++const MAX_OUTPUT = 8000;++const MAX_PATCH = 200000;++++/** Where Playwright may live: the project's own, then what the owner names. */++const PLAYWRIGHT_PATHS = [++ null, // the repo's own node_modules++ '/home/workstation/openvibe/work/OpenVibe.Games/node_modules/playwright',++];++++/** Playwright, or null when this machine has none. */++function findPlaywright(env = {}) {++ const named = env.CODES_PLAYWRIGHT_PATH || env.PLAYWRIGHT_PATH;++ const tries = named ? [named, ...PLAYWRIGHT_PATHS] : PLAYWRIGHT_PATHS;++ for (const p of tries) {++ try {++ const mod = p ? require(p) : require('playwright');++ if (mod && mod.chromium) return mod;++ } catch { /* try the next one */ }++ }++ return null;++}++++/** A command in the repo: no shell, a timeout, and the output it printed. */++function commandRunner({ env = process.env } = {}) {++ return async function run(argv, { cwd, timeoutMs = 120000 } = {}) {++ if (!Array.isArray(argv) || !argv.length) return { code: 1, output: 'no command' };++ return await new Promise((resolve) => {++ const child = execFile(argv[0], argv.slice(1), {++ cwd, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024,++ env: { ...env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo' },++ }, (err, stdout, stderr) => {++ const code = err && typeof err.code === 'number' ? err.code : (err ? 1 : 0);++ const out = String(stdout || '') + (stderr ? '\n' + String(stderr) : '');++ resolve({ code, output: out.length > MAX_OUTPUT ? out.slice(-MAX_OUTPUT) : out });++ });++ });++ };++}++++/** The diff against HEAD: the stats a person reads, and the patch a reviewer reads. */++function diffAdapter(run) {++ return async function diff({ cwd } = {}) {++ const where = cwd || run.cwd;++ const stats = await run(['git', 'diff', '--stat', 'HEAD'], { cwd: where });++ const names = await run(['git', 'diff', '--name-only', 'HEAD'], { cwd: where });++ const patch = await run(['git', 'diff', '--no-color', 'HEAD'], { cwd: where });++ const files = String(names.output || '').split('\n').map((s) => s.trim()).filter(Boolean);++ return {++ files,++ stats: String(stats.output || '').trim(),++ patch: String(patch.output || '').slice(0, MAX_PATCH),++ summary: files.length ? files.length + ' file' + (files.length === 1 ? '' : 's') + ' changed' : 'nothing changed',++ };++ };++}++++/**++ * One page at one width: the screenshot, the sideways scroll and the console errors.++ *++ * @returns {Promise<object>} { ok, path, summary, width, errors, overflow }++ */++async function shootPage({ playwright, chromePath, url, width, outDir, route }) {++ const browser = await playwright.chromium.launch({ executablePath: chromePath, args: ['--no-sandbox'] });++ try {++ const page = await browser.newPage({++ viewport: { width, height: 900 }, deviceScaleFactor: 1,++ isMobile: width < 700, hasTouch: width < 700,++ });++ const errors = [];++ const consoleErrors = [];++ page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e).slice(0, 300)));++ // Console errors are kept, not fatal: a blocked third-party request (a frame, a theme, a font) is the network++ // around the page, not a bug in it. An uncaught exception is the page's own, and that one fails the proof.++ page.on('console', (m) => { if (m && m.type() === 'error') consoleErrors.push(String(m.text()).slice(0, 300)); });++ await page.goto(url, { waitUntil: 'load', timeout: 30000 });++ await page.waitForTimeout(600);++ const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);++ const file = path.join(outDir, `proof-${(route || '/').replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '') || 'home'}-${width}.png`);++ await page.screenshot({ path: file, fullPage: true });++ const overflow = Number(scrollWidth) > width;++ const ok = !overflow && !errors.length;++ return {++ ok, path: file, width, route: route || '/', errors, console_errors: consoleErrors, overflow, url,++ final_url: typeof page.url === 'function' ? page.url() : url,++ summary: ok ? 'the page renders clean'++ : [overflow ? 'the page is ' + scrollWidth + 'px wide, so it scrolls sideways on a ' + width + 'px screen' : null]++ .concat(errors.length ? ['the page threw: ' + errors.slice(0, 3).join('; ')] : []).filter(Boolean).join('; '),++ };++ } finally {++ await browser.close().catch(() => {});++ }++}++++/** A port nothing is using, so the repo's site does not collide with what the person already runs. */++function freePort() {++ return new Promise((resolve, reject) => {++ const probe = net.createServer();++ probe.on('error', reject);++ probe.listen(0, '127.0.0.1', () => {++ const port = probe.address().port;++ probe.close(() => resolve(port));++ });++ });++}++++/** Wait until the site answers: a redirect or a 401 is up; a 5xx or no answer is not. */++async function waitForSite(url, { fetchImpl = globalThis.fetch, timeoutMs = 30000, everyMs = 250 } = {}) {++ const until = Date.now() + timeoutMs;++ let last = null;++ while (Date.now() < until) {++ try {++ const res = await fetchImpl(url, { redirect: 'manual' });++ if (res && res.status < 500) return { ok: true, status: res.status };++ last = res ? 'HTTP ' + res.status : 'no response';++ } catch (e) {++ last = e && e.message ? e.message : String(e);++ }++ await new Promise((r) => setTimeout(r, everyMs));++ }++ return { ok: false, why: last || 'no answer' };++}++++/**++ * The repo's own site, up: the URL it answers on, and how to stop it.++ *++ * CODES_PROOF_URL names a site that is already running (a dev server, a preview). Otherwise the repo's own start++ * script runs on a free port in its own process group, so the group can be stopped without touching anything else++ * the person runs. The script is `npm start` unless CODES_PROOF_START names another command.++ *++ * @returns {Promise<{ url: string, stop: function }>} the site++ */++async function startSite({ cwd, env = process.env, fetchImpl = globalThis.fetch, spawnFn = spawn, timeoutMs = 30000 } = {}) {++ const named = String(env.CODES_PROOF_URL || '').trim();++ if (named) {++ const url = named.replace(/\/+$/, '');++ const ready = await waitForSite(url, { fetchImpl, timeoutMs });++ if (!ready.ok) {++ const err = new Error('the site at ' + url + ' did not answer: ' + ready.why);++ err.code = 'site.not_ready';++ throw err;++ }++ return { url, stop: async () => {} };++ }++ const port = await freePort();++ const url = 'http://127.0.0.1:' + port;++ const [command, ...args] = String(env.CODES_PROOF_START || 'npm start').split(/\s+/).filter(Boolean);++ const child = spawnFn(command, args, {++ cwd, env: { ...env, PORT: String(port), HOST: '127.0.0.1' }, stdio: 'ignore', detached: true,++ });++ const stop = async () => {++ try { process.kill(-child.pid, 'SIGTERM'); } catch { try { child.kill('SIGTERM'); } catch { /* it is gone */ } }++ };++ const ready = await waitForSite(url, { fetchImpl, timeoutMs });++ if (!ready.ok) {++ await stop();++ const err = new Error('the repo\'s site did not start on ' + url + ' (' + ready.why + ')');++ err.code = 'site.not_ready';++ throw err;++ }++ return { url, stop };++}++++/**++ * The screenshots: the pages the change touches, at a phone's and a desktop's width.++ *++ * A page that is a file is shot from the working tree (a file:// URL), so a static site needs nothing running. A page++ * that only exists once the app renders it (server/http/run.js, a template) has no file to open: the repo's own start++ * script is brought up on a free port and the routes are shot there. If the site cannot start, the step says so++ * instead of pretending a page was looked at.++ */++function screenshotAdapter({ run, playwright, chromePath, outDir, env = process.env, fetchImpl = globalThis.fetch, startSiteFn = startSite }) {++ return async function screenshot({ files = [], routes = [], width = 390, cwd, pkg = null } = {}) {++ if (!playwright) return { ok: true, skipped: true, summary: 'this machine has no browser to shoot the pages with' };++ const where = cwd || run.cwd;++ const all = Array.isArray(files) ? files : [];++ const staticPages = all.filter((f) => /\.html?$/i.test(String(f)));++ const rendered = all.filter((f) => !/\.html?$/i.test(String(f)));++ const canServe = !!(env.CODES_PROOF_URL || (pkg && pkg.scripts && typeof pkg.scripts.start === 'string'));++ const wantSite = canServe && rendered.length > 0;++ if (!staticPages.length && !wantSite) {++ return { ok: true, skipped: true, summary: 'no HTML page changed, and the repo has no site to render one' };++ }++ fs.mkdirSync(outDir, { recursive: true });++ const shots = [];++ for (const file of staticPages.slice(0, 5)) {++ const abs = path.resolve(where, file);++ if (!fs.existsSync(abs)) continue;++ try {++ shots.push(await shootPage({ playwright, chromePath, url: 'file://' + abs, width, outDir, route: file }));++ } catch (e) {++ shots.push({ ok: false, width, route: file, summary: 'the page could not be shot: ' + e.message });++ }++ }++ if (wantSite) {++ let site = null;++ try {++ site = await startSiteFn({ cwd: where, env, fetchImpl });++ for (const route of (routes.length ? routes : ['/']).slice(0, 5)) {++ const url = site.url.replace(/\/+$/, '') + (route.startsWith('/') ? route : '/' + route);++ try {++ shots.push({ live: true, ...(await shootPage({ playwright, chromePath, url, width, outDir, route })) });++ } catch (e) {++ shots.push({ ok: false, live: true, width, route, summary: 'the page could not be shot: ' + e.message });++ }++ }++ } catch (e) {++ // The change touches a page this repo renders; the site did not come up, so nobody looked at it.++ return { ok: true, skipped: true, summary: 'the site did not start, so the pages were not shot: ' + e.message };++ } finally {++ if (site) await site.stop();++ }++ }++ if (!shots.length) return { ok: true, skipped: true, summary: 'no page was there to shoot' };++ const bad = shots.filter((s) => s.ok === false);++ return {++ ok: !bad.length, skipped: false,++ path: shots[0].path, shots,++ summary: bad.length ? bad.map((s) => s.width + 'px ' + s.route + ': ' + s.summary).join('; ')++ : shots.length + ' page' + (shots.length === 1 ? '' : 's') + ' at ' + width + 'px, clean',++ };++ };++}++++/** The OpenAI-compatible root a run token should be spent at: the job's own endpoint names, then the machine's. */++function aiRoot(env = {}) {++ if (env.OPENVIBE_AI_URL) return env.OPENVIBE_AI_URL;++ if (env.OPENAI_BASE_URL) return env.OPENAI_BASE_URL;++ if (env.DEEPSEEK_BASE_URL) return env.DEEPSEEK_BASE_URL;++ if (env.ANTHROPIC_BASE_URL) return String(env.ANTHROPIC_BASE_URL).replace(/\/+$/, '') + '/v1';++ return '';++}++++/**++ * The second opinion: OpenVibe.AI, with the task's run token.++ *++ * The token names the person and comes with the attempt (stage C3 mints it for the harness and puts it in the job's++ * input). It is read from the job's own input, never from every provider key in the machine's environment: a personal++ * OPENAI_API_KEY is not Codes' to spend at OpenVibe.AI. Without a token there is no second opinion, and the adapter++ * says so rather than pretending one happened.++ */++function reviewAdapter({ env = {}, jobEnv = {}, fetchImpl = globalThis.fetch, baseUrl, models = [] } = {}) {++ return async function review({ patch = '', summary = '', tests = null, model = null, family = null } = {}) {++ // The token the attempt's own harness carries: any of the names C3 gives, that harness's provider first.++ const runToken = jobEnv.OPENVIBE_AI_TOKEN || jobEnv.OPENAI_API_KEY || jobEnv.ANTHROPIC_AUTH_TOKEN || jobEnv.DEEPSEEK_API_KEY || '';++ const token = runToken || env.OPENVIBE_CODES_TASK_TOKEN || env.OPENVIBE_AI_TOKEN || '';++ if (!token) {++ const err = new Error('no task token is connected yet, so no model could review this change');++ err.code = 'review.no_token';++ throw err;++ }++ const chosen = model || (models.find((m) => familyOf(m) && familyOf(m) !== family) || null);++ if (!chosen) {++ const err = new Error('no model of another family is connected, so nobody could review this change');++ err.code = 'review.no_model';++ throw err;++ }++ return await reviewWithAi({++ baseUrl: baseUrl || aiRoot(jobEnv) || env.OPENVIBE_AI_URL || 'https://ai.openvibe.services/v1',++ token, model: chosen, family: familyOf(chosen), patch, summary, tests, fetchImpl,++ });++ };++}++++/**++ * The adapters, for one repo.++ *++ * @param {object} what++ * @param {string} what.cwd the repo the change is in++ * @param {object} [what.env] the environment++ * @param {function} [what.fetchImpl] fetch, for a test++ * @param {string} [what.chromePath] the Chrome to shoot the pages with++ * @param {string} [what.outDir] where the screenshots are written++ * @returns {object} the adapters++ */++function createProofAdapters({ cwd, env = process.env, jobEnv = {}, fetchImpl = globalThis.fetch, chromePath = null, outDir = null, playwright = null, models = null, startSiteFn = startSite } = {}) {++ const run = commandRunner({ env });++ run.cwd = cwd;++ const pw = playwright || findPlaywright(env);++ const chrome = chromePath || env.CODES_CHROME_PATH || '/usr/bin/google-chrome';++ const out = outDir || path.join(cwd, '.openvibe-proof');++ // The models a reviewer may be chosen from: the catalog of the machine, or what the owner names.++ const named = Array.isArray(models) && models.length ? models : String(env.CODES_PROOF_REVIEW_MODELS || '').split(',').map((s) => s.trim()).filter(Boolean);++ return {++ run,++ diff: diffAdapter(run),++ screenshot: screenshotAdapter({ run, playwright: pw, chromePath: chrome, outDir: out, env, fetchImpl, startSiteFn }),++ review: reviewAdapter({ env, jobEnv, fetchImpl, models: named }),++ models: named,++ env,++ };++}++++module.exports = {++ createProofAdapters, commandRunner, diffAdapter, screenshotAdapter, reviewAdapter,++ findPlaywright, shootPage, freePort, waitForSite, startSite,++};nnew file mode 100644@@@ -0,0 +1,30 @@++-- phase: expand++-- Stage C8 (openvibe.codes v1): the proof of a finished task. "Proof, not promises": every task ends with its tests,++-- its screenshots, its diff and a second model's review.++--++-- The items live in codes_task_proof, one row per proof item of one attempt: the tests the change reached, the pages++-- it touched (phone and desktop), the diff, and the review. The task's row keeps the outcome in plain words++-- ("Done with proof", "Didn't finish, with what is left", "Stopped by you") in run_tasks.outcome / outcome_note, as++-- codes.task@2 states it.++--++-- The number is 0012 so it follows the run tokens of stage C3 (0011) without colliding with them.++++CREATE TABLE IF NOT EXISTS codes_task_proof (++ id text COLLATE "C" PRIMARY KEY, -- prf_<ULID>++ task_id text COLLATE "C" NOT NULL, -- tsk_<ULID>++ attempt_n integer NOT NULL DEFAULT 0, -- the attempt the proof belongs to (0: the task itself)++ n integer NOT NULL, -- the item's place in the proof (1, 2, …)++ kind text COLLATE "C" NOT NULL, -- tests | screenshots | diff | review++ label text COLLATE "C" NOT NULL, -- what a person reads: 'tests', 'screenshots', …++ ok boolean NOT NULL, -- did it pass++ skipped boolean NOT NULL DEFAULT false, -- it could not run here (and why is in summary)++ summary text COLLATE "C", -- one line, in plain words++ family text COLLATE "C", -- the reviewer's model family++ detail jsonb, -- the item's own data (shots, stats, the patch)++ created_at timestamptz NOT NULL DEFAULT now()++);++-- The fence: one proof item of one kind per attempt, so a second ending of the same attempt (the portal's++-- 10-30 bug) writes no second row and no second event.++CREATE UNIQUE INDEX IF NOT EXISTS codes_task_proof_item ON codes_task_proof (task_id, attempt_n, kind);++CREATE UNIQUE INDEX IF NOT EXISTS codes_task_proof_task_n ON codes_task_proof (task_id, n);++CREATE INDEX IF NOT EXISTS codes_task_proof_task ON codes_task_proof (task_id, attempt_n);@@@ -33,3 +33,11 @@.run-patch .hunk { color: var(--accent-light, #60a5fa); }.run-repo { overflow-wrap: break-word; }.run-host { display: block; font-size: 0.55em; font-weight: 500; color: var(--text-muted, #8b9bb4); }++++/* The proof of a finished task (stage C8): the tests, the pages, the diff and the review. */++.run-proof { margin: 16px 0; max-width: 820px; }++.run-proof-items { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }++.run-proof-item { border: 1px solid var(--border, #1f2d47); border-radius: 12px; padding: 10px 14px; background: var(--bg-card, #131c2e); }++.run-proof-head { margin: 0; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }++.run-proof-summary { margin: 4px 0 0; color: var(--text-muted, #7386a3); overflow-wrap: anywhere; }++.run-proof-item[data-ok="false"] { border-color: var(--danger, #e5484d); }nnew file mode 100644@@@ -0,0 +1,144 @@++'use strict';++++/**++ * A finished task's proof: its tests, its pages, its diff and the second model's opinion.++ *++ * The proof arrives with the machine's result (the job worker collected it there, in the clone the agent worked in),++ * and this module keeps it: one row per proof item, one event per item on the task's stream, and the task's outcome++ * in the plain words a person reads.++ *++ * createProof({ store, config, log })++ * record(taskId, attemptN, items) the items of one attempt: the rows, and the lines for the stream++ * list(taskId) the items of a task, in order++ * present(items) the items as the API shows them++ * outcomeOf(items, fallback) the outcome in plain words++ *++ * The outcome is what the spec asks for: "Done with proof", "Didn't finish, with what is left" and "Stopped by++ * you". The engine's own verdict stands (a task the agent did not finish is unfinished); the proof says out loud++ * what the evidence says, so a task that claims to be done and whose tests fail does not pass for done.++ */++++const { proofNote } = require('../../engine/proof');++++/** The kinds of proof item, in the order they are read. */++const KINDS = ['tests', 'screenshots', 'diff', 'review'];++++/** How much of an item's own data is kept. */++const MAX_DETAIL = 64 * 1024;++++const iso = (v) => {++ if (!v) return null;++ const t = v instanceof Date ? v.getTime() : Date.parse(String(v));++ return Number.isFinite(t) ? new Date(t).toISOString() : null;++};++++const clip = (s, n) => String(s == null ? '' : s).slice(0, n);++++function createProof({ store, config = {}, log = console }) {++ const db = store.db;++++ /**++ * The proof of one attempt: the rows, and the lines the task's stream shows (one per item).++ *++ * Recording the same attempt twice changes nothing: the row's place in the proof is fixed (the attempt's number++ * and the item's), so a second recording finds the rows there and writes no second event.++ *++ * @param {string} taskId the task++ * @param {number|null} attemptN the attempt the proof belongs to++ * @param {object[]} items the proof items++ * @returns {Promise<{ rows: object[], lines: object[] }>} the rows, and the lines to append to the stream++ */++ async function record(taskId, attemptN, items) {++ const list = Array.isArray(items) ? items : [];++ if (!taskId || !list.length) return { rows: [], lines: [] };++ const known = await db.many('SELECT id, n FROM codes_task_proof WHERE task_id = $1', [taskId]);++ const seen = new Set(known.map((r) => Number(r.n)));++ const attempt = attemptN == null ? 0 : Number(attemptN);++ const stored = [];++ for (const item of list) {++ const kind = KINDS.includes(item && item.kind) ? item.kind : 'note';++ let n = stored.length + 1;++ while (seen.has(n)) n++;++ seen.add(n);++ const id = store.newId('prf');++ const detail = item && item.detail !== undefined ? item.detail : item;++ let body = null;++ try { body = JSON.stringify(detail); } catch { body = null; }++ if (body && body.length > MAX_DETAIL) body = body.slice(0, MAX_DETAIL);++ const row = await db.maybe(++ 'INSERT INTO codes_task_proof (id, task_id, attempt_n, n, kind, label, ok, skipped, summary, family, detail, created_at)'++ + ' VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12::timestamptz)'++ + ' ON CONFLICT DO NOTHING RETURNING *',++ [id, taskId, attempt, n, kind, clip(item && item.label, 80),++ item && item.ok === false ? false : true, item && item.skipped === true ? true : false,++ clip(item && item.summary, 1000), clip(item && item.family, 80), body, store.iso()],++ );++ if (row) stored.push(row);++ }++ // One line per item on the task's stream, after the lines that are already there.++ let seq = await nextSeq(taskId);++ const at = store.iso();++ const lines = stored.map((row) => ({++ seq: seq++, at, kind: 'proof',++ body: {++ t: 'proof', kind: row.kind, label: row.label, ok: row.ok === true,++ skipped: row.skipped === true, summary: row.summary || null, family: row.family || null,++ },++ }));++ return { rows: stored, lines };++ }++++ /** The first seq on the task's stream that nothing uses. */++ async function nextSeq(taskId) {++ const rows = await db.many('SELECT seq FROM codes_session_lines WHERE task_id = $1', [taskId]);++ const taken = new Set(rows.map((r) => Number(r.seq)));++ const max = rows.reduce((m, r) => Math.max(m, Number(r.seq) || 0), 0);++ let seq = max + 1;++ let tries = 0;++ while (taken.has(seq) && tries++ < 10000) seq++;++ return seq;++ }++++ /** The items of a task, in order. */++ async function list(taskId) {++ if (!taskId) return [];++ return await db.many('SELECT * FROM codes_task_proof WHERE task_id = $1 ORDER BY n ASC', [taskId]);++ }++++ /** The items as the API shows them. */++ function present(items) {++ return (Array.isArray(items) ? items : []).map((r) => ({++ kind: r.kind,++ label: r.label,++ ok: r.ok === true,++ skipped: r.skipped === true,++ summary: r.summary || null,++ family: r.family || null,++ attempt_n: r.attempt_n == null ? null : Number(r.attempt_n),++ detail: r.detail || null,++ created_at: iso(r.created_at),++ }));++ }++++ /**++ * The outcome in plain words, from the proof.++ *++ * @param {object[]} items the proof items++ * @param {object} [fallback] { state, note } the outcome without a proof (the engine's own verdict)++ * @returns {{ state: string, note: string }} the outcome++ */++ function outcomeOf(items, fallback = {}) {++ const list = Array.isArray(items) ? items : [];++ if (!list.length) return { state: fallback.state || null, note: fallback.note || null };++ const failed = list.filter((i) => i && i.ok === false);++ const note = proofNote(list);++ // Proof, not promises: when the evidence failed, the task is not called done, whatever the agent claimed. A task++ // the person stopped stays stopped: that outcome is theirs, not the evidence's.++ const state = fallback.state === 'stopped' ? 'stopped' : (failed.length ? 'unfinished' : (fallback.state || 'done'));++ return { state, note: note + (fallback.note ? ' ' + fallback.note : '') };++ }++++ return { KINDS, record, list, present, outcomeOf };++}++++module.exports = { createProof, KINDS };@@@ -140,12 +140,14 @@ function usageOf(result) {};}--function createRunTasks({ store, run, config, log = console, sessions = null, outbox = null, runTokens = null, connections = null, attempts = null }) {++function createRunTasks({ store, run, config, log = console, sessions = null, outbox = null, runTokens = null, connections = null, attempts = null, proof = null }) {const db = store.db;const who = (row) => ({ subject: row.subject, project: row.project_id });// Lazily, so a caller that only reads tasks does not need the session/outbox modules (and there is no cycle).const sessionLines = sessions || require('./session-lines').createSessionLines({ store, config, log });const taskOutbox = outbox || require('../events/outbox').createOutbox({ store, log });++ // The proof of a finished task (stage C8): the tests, the pages, the diff and the second model's opinion.++ const taskProof = proof || require('./proof').createProof({ store, config, log });const attemptStore = attempts || require('./attempts').createAttempts({ store, config, log });/** An attempt's state when it ended: a limit reached is a budget-stop, never a plain failure. */const ENDED = new Set(['done', 'failed', 'stopped', 'budget-stopped', 'interrupted']);@@@ -389,16 +391,40 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ouconst attempts = await attemptStore.list(row.id);const ending = attempts[attempts.length - 1];if (!ending || !ENDED.has(ending.state)) return row;++ // The proof this attempt's machine collected: the items are kept, and each one is an event on the stream.++ const collected = await recordProof(row, ending);const plan = await attemptStore.decide({ task: row, attempts, ending, result: row.result });if (plan.outcome) {++ // The outcome in plain words: what the engine decided, and what the proof says about it.++ const outcome = taskProof.outcomeOf(collected, { state: plan.outcome, note: plan.note });return await db.one('UPDATE run_tasks SET outcome = $2, outcome_note = $3, updated_at = $4 WHERE id = $1 RETURNING *',-- [row.id, plan.outcome, plan.note, store.now()]);++ [row.id, outcome.state, outcome.note, store.now()]);}if (!plan.kind) return row;const done = await startFollowUp(row, plan, ending);return done.row || row;}++ /**++ * The proof this attempt's machine collected: the items are kept, and each one is an event on the task's stream.++ *++ * The machine collects it in the clone it worked in (harness/job.js), so the tests that ran and the pages that were++ * shot are about that machine's work. A task from before this change has no proof, and says nothing about one.++ *++ * @param {object} row the task++ * @param {object} ending the attempt that ended++ * @returns {Promise<object[]>} the items as they are stored++ */++ async function recordProof(row, ending) {++ const result = asObject(row.result) || {};++ const items = Array.isArray(result.proof && result.proof.items) ? result.proof.items : [];++ if (!items.length) return [];++ const attemptN = ending ? Number(ending.n) : null;++ const recorded = await taskProof.record(row.id, attemptN, items);++ if (recorded.lines.length) await sessionLines.append(row.id, recorded.lines, attemptN);++ return recorded.rows;++ }++/*** The next attempt: a Run job for the task with the engine's plan in it. The attempt is written first and the job* second, so the fence (follow_up_of) is in place before anything is started.@@@ -542,7 +568,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou}return {-- start, refresh, applyJob, applyEvent, recordState, afterEnd, continueTask, stopTask,++ start, refresh, applyJob, applyEvent, recordState, afterEnd, continueTask, stopTask, proof: taskProof,latestProject, present, who, attempts: attemptStore,};}@@@ -190,8 +190,13 @@ ${typeof d.duration_ms === 'number' ? html`<p class="muted">${Math.round(d.duratasync function timelineHtml(row) {const attempts = await taskStore.attempts.list(row.id);if (!attempts.length) return '';++ // The outcome in plain words: the label, and the note beside it — unless the note already says it++ // ("Done with proof: …" says "Done" already, so it is not said twice).++ const note = row.outcome_note || '';++ const label = row.outcome ? (OUTCOME_TEXT[row.outcome] || row.outcome) : '';++ const says = !!label && note.toLowerCase().startsWith(label.toLowerCase());const outcome = row.outcome-- ? html`<p class="run-outcome" data-outcome="${row.outcome}"><b>${OUTCOME_TEXT[row.outcome] || row.outcome}</b>${row.outcome_note ? html` ${row.outcome_note}` : ''}</p>`++ ? html`<p class="run-outcome" data-outcome="${row.outcome}"><b>${says ? note : [label, note].filter(Boolean).join(' ')}</b></p>`: '';return html`<section class="run-timeline" data-timeline data-attempts="${attempts.length}"><h2>The attempts</h2>@@@ -204,6 +209,23 @@ ${outcome}</section>`;}++ /**++ * The task's proof (stage C8): the tests, the pages, the diff and the second model's opinion, each with what it++ * found. The outcome in plain words sits above it, so a person reads what was proved before the detail.++ */++ async function proofHtml(row) {++ const items = await taskStore.proof.list(row.id);++ if (!items.length) return '';++ const shown = taskStore.proof.present(items);++ return html`<section class="run-proof" data-proof data-items="${shown.length}">++<h2>The proof</h2>++<ul class="run-proof-items">${shown.map((i) => html`<li class="run-proof-item" data-proof-kind="${i.kind}" data-ok="${i.ok}" data-skipped="${i.skipped}">++<p class="run-proof-head"><b>${i.label}</b>${i.skipped ? html` <span class="run-pill">skipped</span>` : (i.ok ? html` <span class="run-pill run-pill-succeeded">passed</span>` : html` <span class="run-pill run-pill-failed">failed</span>`)}${i.family ? html` · ${i.family}` : ''}</p>++${i.summary ? html`<p class="run-proof-summary" data-proof-summary>${i.summary}</p>` : ''}++</li>`)}</ul>++</section>`;++ }++r.get('/run/tasks/:id', async (req, res) => {if (!signedIn(req)) return res.redirect(302, `/auth/login?next=${encodeURIComponent(req.originalUrl)}`);let row = await own(req, req.params.id);@@@ -220,6 +242,7 @@ ${outcome}<p><span class="run-pill run-pill-${row.state}" data-state>${STATE_LABEL[row.state] || row.state}</span>${row.agent ? html` <span class="run-pill">${row.agent}</span>` : ''}</p><pre tabindex="0" class="run-prompt">${row.prompt}</pre>${await timelineHtml(row)}++${await proofHtml(row)}${live ? html`<section class="run-live" data-events="/run/tasks/${row.id}/events" data-title="${title}" aria-live="polite"><h2>Live</h2><ol class="run-log" data-log></ol></section><form method="post" action="/run/tasks/${row.id}/cancel"><button class="run-btn" type="submit">Cancel</button></form>` : html`${session}${resultHtml(row, { session: !!(lines && lines.length) })}`}`;return page(req, res, { title: 'Task', body, sharedScripts: ['agent-session.js'] });@@@ -96,11 +96,38 @@ function createTaskEvents({ store, config = {}, log = console, attempts = null }};await sendLines();++ // The proof of the task (stage C8): one event per item — the tests, the pages, the diff, the review.++ const sentProof = new Set();++ const sendProof = async () => {++ const items = await db.many('SELECT * FROM codes_task_proof WHERE task_id = $1 ORDER BY n ASC', [row.id]);++ let wrote = 0;++ for (const item of items) {++ if (sentProof.has(item.id)) continue;++ sentProof.add(item.id);++ wrote++;++ write(frame('proof', {++ id: item.id,++ n: Number(item.n),++ kind: item.kind,++ label: item.label,++ ok: item.ok === true,++ skipped: item.skipped === true,++ summary: item.summary || null,++ family: item.family || null,++ attempt_n: item.attempt_n == null ? null : Number(item.attempt_n),++ at: item.created_at ? new Date(item.created_at).toISOString() : null,++ }));++ }++ return wrote;++ };++ await sendProof();++const tick = async () => {if (closed || busy) return;busy = true;try {const sentLines = await sendLines();++ await sendProof();const fresh = await db.maybe('SELECT * FROM run_tasks WHERE id = $1', [row.id]);const rows = await attemptStore.list(row.id);let updates = '';@@@ -11,7 +11,7 @@ const DIR = path.join(__dirname, '..', 'engine');(async () => {await check('the engine exports every module and the names a caller wants', () => {-- for (const name of ['router', 'chain', 'budget', 'transcript', 'effort', 'scope', 'runerror']) {++ for (const name of ['router', 'chain', 'budget', 'transcript', 'effort', 'scope', 'runerror', 'proof', 'review']) {assert.ok(engine[name], `${name} is exported`);}for (const name of ['classify', 'pick', 'outcome', 'nextStep', 'evaluate', 'limitsFor', 'parse', 'parseText', 'fitEffort', 'pickModel']) {nnew file mode 100644@@@ -0,0 +1,188 @@++'use strict';++++/**++ * Stage C8, the engine half: the proof collector (engine/proof.js) and the second opinion (engine/review.js).++ *++ * The collector asks for four things — the repo's own tests, the pages the diff touches, the diff, and a model of++ * another family — and every one of them is an adapter, so a fake stands in for the machine here: the tests never run,++ * no browser opens and no model is called.++ */++const assert = require('assert');++const { collectProof, testPlan, testWords, pageFiles, routesOf, outcomeOf } = require('../engine/proof');++const { check, done } = require('./helpers/boot');++++(async () => {++ /** A fake machine: the repo's package.json, a passing test run, a clean page and a second model's opinion. */++ const machine = ({ pkg = { scripts: { test: 'node test/run.js' } }, files = [], testExit = 0, review = null, shots = [] } = {}) => {++ const calls = [];++ const run = async (argv, options) => {++ calls.push(argv);++ if (argv[0] === 'node' && argv[1] === '-e') return { code: 0, output: JSON.stringify(pkg) };++ if (argv[0] === 'node' && argv[1] === 'test/run.js') return { code: testExit, output: testExit === 0 ? '12/12 test files passed' : 'FAIL test/foo.test.js\nAssertionError' };++ if (argv[0] === 'npm') return { code: testExit, output: testExit === 0 ? '12/12 test files passed' : 'FAIL test/foo.test.js\nAssertionError' };++ return { code: 0, output: '' };++ };++ return {++ calls,++ adapters: {++ run,++ diff: async () => ({ files, stats: files.length + ' files changed', patch: '+ the change', summary: files.length + ' files changed' }),++ screenshot: async ({ width }) => (shots.find((s) => s.width === width) || { ok: true, path: `/proof/${width}.png`, summary: 'the page renders clean' }),++ review: review || (async () => ({ family: 'deepseek', verdict: 'approved', summary: 'It holds together.' })),++ models: ['claude-sonnet-4', 'deepseek-chat', 'gpt-5'],++ env: {},++ },++ };++ };++++ try {++ await check('the collector asks for the four things, in order, and says what each one found', async () => {++ const m = machine({ files: ['engine/proof.js', 'public/index.html'] });++ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters, author: { agent: 'claude-code', model: 'claude-sonnet-4' } });++ assert.deepStrictEqual(proof.items.map((i) => i.kind), ['tests', 'screenshots', 'diff', 'review']);++ assert.strictEqual(proof.outcome.state, 'done');++ assert.ok(/^Done with proof/.test(proof.outcome.note), proof.outcome.note);++ assert.strictEqual(proof.review.family, 'deepseek');++ });++++ await check('the repo\'s own runner is used, narrowed to the tests the change reaches', async () => {++ const m = machine({ files: ['engine/proof.js'] });++ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });++ const test = proof.items.find((i) => i.kind === 'tests');++ assert.strictEqual(test.ok, true);++ assert.strictEqual(test.command, 'node test/run.js proof');++ assert.strictEqual(test.mode, 'targeted');++ });++++ await check('a test script with its own flags is left to npm, and the words follow --', async () => {++ const plan = testPlan({ pkg: { scripts: { test: 'node --test test/' } }, files: ['engine/proof.js'], env: {} });++ assert.strictEqual(plan.command, 'npm');++ assert.deepStrictEqual(plan.args, ['test', '--', 'proof']);++ });++++ await check('the whole suite runs only when the change touches what every test shares and the guard allows it', async () => {++ const shared = testPlan({ pkg: { scripts: { test: 'node test/run.js' } }, files: ['package.json'], env: {} });++ assert.strictEqual(shared.mode, 'targeted');++ assert.ok(/full suite is not allowed/.test(shared.reason), shared.reason);++ const allowed = testPlan({ pkg: { scripts: { test: 'node test/run.js' } }, files: ['package.json'], env: { CODES_PROOF_FULL_SUITE: '1' } });++ assert.strictEqual(allowed.mode, 'full');++ assert.deepStrictEqual(allowed.args, ['test/run.js']);++ });++++ await check('a repo with no test command of its own says so instead of failing', async () => {++ const m = machine({ pkg: { scripts: {} } });++ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });++ const test = proof.items.find((i) => i.kind === 'tests');++ assert.strictEqual(test.skipped, true);++ assert.ok(/no test command/.test(test.summary), test.summary);++ });++++ await check('the pages the diff touches are shot at a phone\'s and a desktop\'s width', async () => {++ const m = machine({ files: ['public/index.html', 'server/http/run.js', 'engine/proof.js'] });++ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });++ const shots = proof.items.find((i) => i.kind === 'screenshots');++ assert.strictEqual(shots.ok, true);++ assert.deepStrictEqual(shots.shots.map((s) => s.width), [390, 1440]);++ });++++ await check('a change that touches no page says so, and does not open a browser', async () => {++ const m = machine({ files: ['engine/proof.js'] });++ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });++ const shots = proof.items.find((i) => i.kind === 'screenshots');++ assert.strictEqual(shots.skipped, true);++ assert.ok(/touches no page/.test(shots.summary), shots.summary);++ });++++ await check('a machine with nothing to shoot with says the screenshots were skipped, and why', async () => {++ const m = machine({ files: ['public/index.html'] });++ m.adapters.screenshot = async ({ width }) => ({ ok: true, skipped: true, width, summary: 'the site did not start, so the pages were not shot' });++ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });++ const shots = proof.items.find((i) => i.kind === 'screenshots');++ assert.strictEqual(shots.skipped, true);++ assert.ok(/did not start/.test(shots.summary), shots.summary);++ assert.ok(/screenshots was skipped/.test(proof.outcome.note), proof.outcome.note);++ });++++ await check('a page that scrolls sideways is a failure of the proof, not a picture of one', async () => {++ const m = machine({ files: ['public/index.html'], shots: [{ width: 390, ok: false, summary: 'the page is 480px wide, so it scrolls sideways on a 390px screen' }] });++ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });++ const shots = proof.items.find((i) => i.kind === 'screenshots');++ assert.strictEqual(shots.ok, false);++ assert.ok(/scrolls sideways/.test(shots.summary), shots.summary);++ assert.strictEqual(proof.outcome.state, 'unfinished');++ assert.ok(/Didn't finish, with what is left/.test(proof.outcome.note), proof.outcome.note);++ });++++ await check('a review by the author\'s own family is refused: it is not a second opinion', async () => {++ const m = machine({ files: ['engine/proof.js'], review: async () => ({ family: 'anthropic', verdict: 'approved', summary: 'Fine.' }) });++ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters, author: { agent: 'claude-code', model: 'claude-sonnet-4' } });++ const review = proof.items.find((i) => i.kind === 'review');++ assert.strictEqual(review.ok, false);++ assert.ok(/own family/.test(review.summary), review.summary);++ assert.strictEqual(proof.review, null);++ });++++ await check('a change request from the second model is said out loud, not hidden', async () => {++ const m = machine({ files: ['engine/proof.js'], review: async () => ({ family: 'deepseek', verdict: 'changes_requested', summary: 'The parser still throws on an empty file.' }) });++ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters, author: { agent: 'claude-code', model: 'claude-sonnet-4' } });++ const review = proof.items.find((i) => i.kind === 'review');++ assert.strictEqual(review.ok, false);++ assert.ok(/empty file/.test(review.summary), review.summary);++ assert.strictEqual(proof.outcome.state, 'unfinished');++ });++++ await check('no reviewer connected is a skip, not a failure', async () => {++ const m = machine({ files: ['engine/proof.js'] });++ const proof = await collectProof({ cwd: '/repo', adapters: { ...m.adapters, review: undefined, models: [] } });++ const review = proof.items.find((i) => i.kind === 'review');++ assert.strictEqual(review.skipped, true);++ assert.strictEqual(proof.outcome.state, 'done');++ });++++ await check('a machine with only the author\'s family says nobody reviewed, and is not called unfinished', async () => {++ const m = machine({ files: ['engine/proof.js'] });++ const proof = await collectProof({++ cwd: '/repo', adapters: { ...m.adapters, models: ['claude-sonnet-4', 'claude-opus-4'] },++ author: { agent: 'claude-code', model: 'claude-sonnet-4' },++ });++ const review = proof.items.find((i) => i.kind === 'review');++ assert.strictEqual(review.skipped, true);++ assert.ok(/no model of another family/.test(review.summary), review.summary);++ assert.strictEqual(proof.outcome.state, 'done');++ assert.ok(/review was skipped/.test(proof.outcome.note), proof.outcome.note);++ });++++ await check('the diff is collected: the files, the stats and the patch', async () => {++ const m = machine({ files: ['engine/proof.js'] });++ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });++ const diff = proof.items.find((i) => i.kind === 'diff');++ assert.deepStrictEqual(diff.files, ['engine/proof.js']);++ assert.strictEqual(diff.patch, '+ the change');++ });++++ await check('every item is emitted as it is collected, so the page can show it live', async () => {++ const m = machine({ files: ['engine/proof.js'] });++ const seen = [];++ await collectProof({ cwd: '/repo', adapters: m.adapters, emit: async (item) => seen.push(item.kind) });++ assert.deepStrictEqual(seen, ['tests', 'screenshots', 'diff', 'review']);++ });++++ await check('the changed files that are pages, and the routes they are served at', async () => {++ assert.deepStrictEqual(pageFiles(['public/index.html', 'engine/proof.js']), ['public/index.html']);++ assert.deepStrictEqual(routesOf(['public/index.html', 'public/a/b.html']), ['/', '/a/b']);++ assert.deepStrictEqual(testWords(['engine/proof.js']), ['proof']);++ });++++ await check('the outcome in plain words: done with proof, or what is left', async () => {++ assert.strictEqual(outcomeOf([{ kind: 'tests', label: 'tests', ok: true }]).state, 'done');++ assert.ok(/Done with proof/.test(outcomeOf([{ kind: 'tests', label: 'tests', ok: true }]).note));++ assert.strictEqual(outcomeOf([{ kind: 'tests', label: 'tests', ok: false, summary: 'it failed' }]).state, 'unfinished');++ });++++ console.log('proof: the tests, the pages, the diff and the second opinion are collected, and said in plain words');++ } catch (e) {++ console.error(e);++ process.exit(1);++ }++ done();++})();nnew file mode 100644@@@ -0,0 +1,95 @@++'use strict';++++/**++ * Stage C8, the second opinion: engine/review.js with a fake fetch.++ *++ * The rule that makes the review worth reading is the family: a model of the family that wrote the change repeats its++ * own reasoning, so its verdict is refused. The call itself is one request to OpenVibe.AI, read as a verdict even++ * when the model answers in prose.++ */++const assert = require('assert');++const {++ familyOf, authorFamily, pickReviewer, reviewPrompt, readVerdict, reviewWithAi,++} = require('../engine/review');++const { check, done } = require('./helpers/boot');++++const OK = async () => ({ status: 200, ok: true, json: async () => ({ choices: [{ message: { content: '{"verdict":"approved","summary":"It holds together."}' } }] }) });++++(async () => {++ try {++ await check('the model families are read from the model\'s own name', () => {++ assert.strictEqual(familyOf('claude-sonnet-4'), 'anthropic');++ assert.strictEqual(familyOf('gpt-5'), 'openai');++ assert.strictEqual(familyOf('deepseek-chat'), 'deepseek');++ assert.strictEqual(familyOf('gemini-2.5-pro'), 'google');++ assert.strictEqual(familyOf('something-else'), null);++ assert.strictEqual(authorFamily({ agent: 'claude-code' }), 'anthropic');++ assert.strictEqual(authorFamily({ agent: 'codex', model: 'gpt-5' }), 'openai');++ });++++ await check('the reviewer is a model of another family', () => {++ const picked = pickReviewer({ models: ['claude-sonnet-4', 'deepseek-chat'], author: 'anthropic' });++ assert.strictEqual(picked.family, 'deepseek');++ assert.strictEqual(pickReviewer({ models: ['claude-sonnet-4'], author: 'anthropic' }), null);++ });++++ await check('a verdict is read from JSON or from the words, and an empty answer is a change request', () => {++ assert.strictEqual(readVerdict('{"verdict":"approved","summary":"ok"}').verdict, 'approved');++ assert.strictEqual(readVerdict('looks good to me').verdict, 'approved');++ assert.strictEqual(readVerdict('this needs changes').verdict, 'changes_requested');++ assert.strictEqual(readVerdict('').verdict, 'changes_requested');++ assert.strictEqual(readVerdict('{"verdict":"changes_requested","summary":"no"}').verdict, 'changes_requested');++ });++++ await check('the prompt carries the change, what it was for, and the tests, and asks for JSON', () => {++ const prompt = reviewPrompt({ patch: '+const x = 1;', summary: 'add x', tests: '3 passed' });++ assert.ok(prompt.includes('+const x = 1;'), 'the diff is in the prompt');++ assert.ok(prompt.includes('add x'), 'what the change was for is in the prompt');++ assert.ok(prompt.includes('3 passed'), 'the tests are in the prompt');++ assert.ok(/approved/.test(prompt) && /changes_requested/.test(prompt), 'the verdict is asked for');++ });++++ await check('the review is one chat completion, with the token, read as a verdict', async () => {++ const calls = [];++ const fetchImpl = async (url, options) => { calls.push({ url, options }); return OK(); };++ const verdict = await reviewWithAi({++ baseUrl: 'https://ai.openvibe.services/v1', token: 'test-token', model: 'deepseek-chat',++ patch: '+x', summary: 'a change', fetchImpl,++ });++ assert.strictEqual(calls.length, 1);++ assert.strictEqual(calls[0].url, 'https://ai.openvibe.services/v1/chat/completions');++ assert.strictEqual(calls[0].options.headers.authorization, 'Bearer test-token');++ assert.strictEqual(JSON.parse(calls[0].options.body).model, 'deepseek-chat');++ assert.strictEqual(verdict.verdict, 'approved');++ assert.strictEqual(verdict.family, 'deepseek');++ });++++ await check('a provider that speaks the messages dialect is tried when chat completions is not there', async () => {++ const calls = [];++ const fetchImpl = async (url) => {++ calls.push(url);++ if (url.endsWith('/chat/completions')) return { status: 404, ok: false, json: async () => ({}) };++ return { status: 200, ok: true, json: async () => ({ content: [{ type: 'text', text: 'changes requested: the parser still throws' }] }) };++ };++ const verdict = await reviewWithAi({ token: 'test-token', model: 'claude-sonnet-4', patch: '+x', fetchImpl });++ assert.ok(calls[1].endsWith('/messages'), calls[1]);++ assert.strictEqual(verdict.verdict, 'changes_requested');++ });++++ await check('a review needs the token and the model, and says which is missing', async () => {++ await assert.rejects(() => reviewWithAi({ model: 'x', fetchImpl: OK }), /token/);++ await assert.rejects(() => reviewWithAi({ token: 't', fetchImpl: OK }), /model/);++ });++++ await check('a refused review is an error, not a verdict', async () => {++ const fetchImpl = async () => ({ status: 429, ok: false, json: async () => ({}) });++ await assert.rejects(() => reviewWithAi({ token: 't', model: 'deepseek-chat', patch: '+x', fetchImpl }), /refused the review \(HTTP 429\)/);++ });++++ console.log('review: a model of another family, one call to OpenVibe.AI, and a verdict read from what it says');++ } catch (e) {++ console.error(e);++ process.exit(1);++ }++ done();++})();nnew file mode 100644@@@ -0,0 +1,237 @@++'use strict';++++/**++ * Stage C8, the machine half: the proof adapters (harness/proof.js) with fakes for every effect.++ *++ * No command runs, no browser opens and no network is touched: the command runner, Playwright, the site and fetch are++ * all fakes, so what is tested is the adapter's own reasoning — the diff's shape, the change going to a site instead++ * of a file, the overflow and the console errors, the reviewer's family, and a token that is never required to try.++ */++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const {++ diffAdapter, screenshotAdapter, reviewAdapter, waitForSite, freePort,++} = require('../harness/proof');++const { check, done } = require('./helpers/boot');++++/** A fake Playwright: one page per viewport, with a scroll width, console errors and page errors to report. */++function fakeBrowser({ scrollWidth = null, consoleErrors = [], pageErrors = [], finalUrl = null } = {}) {++ const shots = [];++ const launched = [];++ return {++ shots, launched,++ chromium: {++ async launch(options) {++ launched.push(options);++ return {++ async close() {},++ async newPage(options) {++ const width = options.viewport.width;++ let at = null;++ const handlers = {};++ return {++ on(name, fn) { (handlers[name] = handlers[name] || []).push(fn); },++ async goto(url) {++ at = url;++ for (const text of consoleErrors) {++ for (const fn of handlers.console || []) fn({ type: () => 'error', text: () => text });++ }++ for (const text of pageErrors) {++ for (const fn of handlers.pageerror || []) fn(new Error(text));++ }++ },++ async waitForTimeout() {},++ async evaluate() { return scrollWidth == null ? width - 1 : scrollWidth; },++ async screenshot({ path: file }) { shots.push({ path: file, width, url: at }); },++ url() { return finalUrl || at; },++ };++ },++ };++ },++ },++ };++}++++/** A fake command runner: git's three questions, answered. */++function fakeRun(answers = {}) {++ const calls = [];++ const run = async (argv) => {++ calls.push(argv.join(' '));++ const key = argv.slice(1).join(' ');++ return { code: 0, output: answers[key] !== undefined ? answers[key] : '' };++ };++ run.calls = calls;++ run.cwd = '/repo';++ return run;++}++++const OK = async () => ({ status: 200, ok: true, json: async () => ({ choices: [{ message: { content: '{"verdict":"approved","summary":"It holds together."}' } }] }) });++++(async () => {++ try {++ await check('the diff is read: the files, the stats and the patch', async () => {++ const run = fakeRun({++ 'diff --stat HEAD': ' a.js | 3 ++-',++ 'diff --name-only HEAD': 'a.js\nb.css\n',++ 'diff --no-color HEAD': '+x\n-y',++ });++ const diff = await diffAdapter(run)({ cwd: '/repo' });++ assert.deepStrictEqual(diff.files, ['a.js', 'b.css']);++ assert.strictEqual(diff.stats, 'a.js | 3 ++-');++ assert.strictEqual(diff.patch, '+x\n-y');++ assert.strictEqual(diff.summary, '2 files changed');++ });++++ await check('a static page is shot from the working tree, at a phone\'s width', async () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-proof-'));++ fs.writeFileSync(path.join(dir, 'index.html'), '<!doctype html><p>hi</p>');++ const browser = fakeBrowser();++ const shot = await screenshotAdapter({++ run: fakeRun(), playwright: browser, chromePath: '/bin/true', outDir: path.join(dir, 'shots'),++ })({ cwd: dir, width: 390, files: ['index.html'], routes: ['/'] });++ assert.strictEqual(shot.ok, true);++ assert.strictEqual(shot.shots.length, 1);++ assert.ok(shot.shots[0].url.startsWith('file://'), shot.shots[0].url);++ });++++ await check('a page only the app renders is shot from the repo\'s own site, which is stopped afterwards', async () => {++ const browser = fakeBrowser();++ const seen = [];++ let stopped = 0;++ const shot = await screenshotAdapter({++ run: fakeRun(), playwright: browser, chromePath: '/bin/true', outDir: path.join(os.tmpdir(), 'codes-proof-shots'),++ startSiteFn: async ({ cwd }) => {++ seen.push(cwd);++ return { url: 'http://127.0.0.1:4321', stop: async () => { stopped++; } };++ },++ })({++ cwd: '/repo', width: 1440, files: ['server/http/run.js'], routes: ['/'],++ pkg: { scripts: { start: 'node server/index.js' } },++ });++ assert.deepStrictEqual(seen, ['/repo']);++ assert.strictEqual(stopped, 1, 'the site is stopped even when it answered');++ assert.strictEqual(shot.ok, true);++ assert.strictEqual(shot.shots[0].url, 'http://127.0.0.1:4321/');++ assert.strictEqual(shot.shots[0].live, true);++ });++++ await check('a site that does not start is said out loud, not turned into a clean screenshot', async () => {++ const browser = fakeBrowser();++ const shot = await screenshotAdapter({++ run: fakeRun(), playwright: browser, chromePath: '/bin/true', outDir: path.join(os.tmpdir(), 'codes-proof-shots'),++ startSiteFn: async () => { const e = new Error('the repo\'s site did not start'); e.code = 'site.not_ready'; throw e; },++ })({ cwd: '/repo', width: 390, files: ['server/http/run.js'], routes: ['/'], pkg: { scripts: { start: 'node server/index.js' } } });++ assert.strictEqual(shot.skipped, true);++ assert.ok(/did not start/.test(shot.summary), shot.summary);++ });++++ await check('a page that scrolls sideways fails the screenshots', async () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-proof-'));++ fs.writeFileSync(path.join(dir, 'index.html'), '<!doctype html><p>wide</p>');++ const shot = await screenshotAdapter({++ run: fakeRun(), playwright: fakeBrowser({ scrollWidth: 480 }), chromePath: '/bin/true', outDir: path.join(dir, 'shots'),++ })({ cwd: dir, width: 390, files: ['index.html'], routes: ['/'] });++ assert.strictEqual(shot.ok, false);++ assert.ok(/scrolls sideways/.test(shot.summary), shot.summary);++ });++++ await check('a page that throws fails the screenshots', async () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-proof-'));++ fs.writeFileSync(path.join(dir, 'index.html'), '<!doctype html><p>hi</p>');++ const shot = await screenshotAdapter({++ run: fakeRun(), playwright: fakeBrowser({ pageErrors: ['TypeError: x is undefined'] }), chromePath: '/bin/true', outDir: path.join(dir, 'shots'),++ })({ cwd: dir, width: 1440, files: ['index.html'], routes: ['/'] });++ assert.strictEqual(shot.ok, false);++ assert.ok(/the page threw/.test(shot.summary), shot.summary);++ });++++ await check('a blocked third-party request is kept as detail, and does not fail the page', async () => {++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-proof-'));++ fs.writeFileSync(path.join(dir, 'index.html'), '<!doctype html><p>hi</p>');++ const shot = await screenshotAdapter({++ run: fakeRun(), playwright: fakeBrowser({ consoleErrors: ['Failed to load resource: net::ERR_FAILED'] }), chromePath: '/bin/true', outDir: path.join(dir, 'shots'),++ })({ cwd: dir, width: 390, files: ['index.html'], routes: ['/'] });++ assert.strictEqual(shot.ok, true);++ assert.deepStrictEqual(shot.shots[0].console_errors, ['Failed to load resource: net::ERR_FAILED']);++ });++++ await check('a machine with no browser says it cannot shoot, instead of failing', async () => {++ const shot = await screenshotAdapter({ run: fakeRun(), playwright: null, chromePath: null, outDir: '/tmp/x' })({ files: ['index.html'] });++ assert.strictEqual(shot.skipped, true);++ assert.ok(/no browser/.test(shot.summary), shot.summary);++ });++++ await check('the review needs the task\'s token, and says so when it has none', async () => {++ const review = reviewAdapter({ env: {}, fetchImpl: OK, models: ['deepseek-chat'] });++ await assert.rejects(() => review({ patch: '+x', family: 'anthropic' }), (e) => e.code === 'review.no_token');++ });++++ await check('the review goes to OpenVibe.AI as chat completions, with the token', async () => {++ const calls = [];++ const fetchImpl = async (url, options) => { calls.push({ url, options }); return OK(); };++ const review = reviewAdapter({ env: { OPENVIBE_CODES_TASK_TOKEN: 'test-token' }, fetchImpl, models: ['deepseek-chat'] });++ const verdict = await review({ patch: '+x', summary: 'a change', family: 'anthropic', model: 'deepseek-chat' });++ assert.strictEqual(verdict.verdict, 'approved');++ assert.strictEqual(verdict.family, 'deepseek');++ assert.ok(calls[0].url.endsWith('/chat/completions'), calls[0].url);++ assert.strictEqual(calls[0].options.headers.authorization, 'Bearer test-token');++ });++++ await check('the review spends the run token the attempt carries, at the endpoint that token names', async () => {++ const calls = [];++ const fetchImpl = async (url, options) => { calls.push({ url, options }); return OK(); };++ const review = reviewAdapter({++ env: { OPENAI_API_KEY: 'a-personal-key' },++ jobEnv: { ANTHROPIC_BASE_URL: 'https://ai.openvibe.services', ANTHROPIC_AUTH_TOKEN: 'run-token' },++ fetchImpl,++ });++ const verdict = await review({ patch: '+x', family: 'anthropic', model: 'deepseek-chat' });++ assert.strictEqual(verdict.verdict, 'approved');++ assert.strictEqual(calls[0].url, 'https://ai.openvibe.services/v1/chat/completions');++ assert.strictEqual(calls[0].options.headers.authorization, 'Bearer run-token');++ });++++ await check('a personal provider key in the environment is not spent as a run token', async () => {++ const review = reviewAdapter({ env: { OPENAI_API_KEY: 'a-personal-key' }, fetchImpl: OK, models: ['deepseek-chat'] });++ await assert.rejects(() => review({ patch: '+x', family: 'anthropic' }), (e) => e.code === 'review.no_token');++ });++++ await check('a provider that speaks the messages dialect is tried when chat completions is not there', async () => {++ const calls = [];++ const fetchImpl = async (url) => {++ calls.push(url);++ if (url.endsWith('/chat/completions')) return { status: 404, ok: false, json: async () => ({}) };++ return { status: 200, ok: true, json: async () => ({ content: [{ type: 'text', text: 'changes requested: the parser still throws' }] }) };++ };++ const review = reviewAdapter({ env: { OPENVIBE_AI_TOKEN: 'test-token' }, fetchImpl });++ const verdict = await review({ patch: '+x', family: 'anthropic', model: 'claude-sonnet-4' });++ assert.ok(calls[1].endsWith('/messages'), calls[1]);++ assert.strictEqual(verdict.verdict, 'changes_requested');++ });++++ await check('the site readiness probe waits through a failure and accepts a redirect', async () => {++ let tries = 0;++ const fetchImpl = async () => {++ tries++;++ if (tries < 3) throw new Error('ECONNREFUSED');++ return { status: 302 };++ };++ const ready = await waitForSite('http://127.0.0.1:1', { fetchImpl, timeoutMs: 5000, everyMs: 5 });++ assert.strictEqual(ready.ok, true);++ assert.strictEqual(tries, 3);++ });++++ await check('a free port is a port a site can answer on', async () => {++ const port = await freePort();++ assert.ok(Number.isInteger(port) && port > 0 && port < 65536, String(port));++ });++++ console.log('harness proof: the diff, the pages (file or site), the overflow and the second opinion, with fakes');++ } catch (e) {++ console.error(e);++ process.exit(1);++ }++ done();++})();nnew file mode 100644@@@ -0,0 +1,169 @@++'use strict';++++/**++ * Stage C8: a finished task's proof. The machine collects it (harness/job.js) and sends it with the result; the++ * server keeps it (one row per item), tells the world one event per item, and says the task's outcome in plain words.++ *++ * The review is behind an interface (engine/review.js): with the connections of stage C3 it is a model of another++ * family through OpenVibe.AI; without them it is a fake, and the fake is what this test uses.++ */++const assert = require('assert');++const { boot, check, done } = require('./helpers/boot');++++(async () => {++ const jobs = new Map();++ let counter = 0;++ const run = {++ enabled: true,++ async submit(o) {++ counter++;++ const id = `job_01J8Z3Q4R5S6T7V8W9X0Y1Z2J${String(counter).padStart(2, '0')}`;++ jobs.set(id, { id, state: 'queued', args: o.args });++ return { id, state: 'queued' };++ },++ async read(jobId) { return jobs.get(jobId) || { id: jobId, state: 'queued' }; },++ async cancel() { throw new Error('not used'); },++ async streamUrl() { throw new Error('not used'); },++ };++ const t = await boot({ run });++ const db = t.ctx.store.db;++ const ada = t.network.addUser('ada');++ const project = 'prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1';++++ // A service's task runs in the project of the person's latest /run task, so Ada needs one before she starts any.++ await db.exec(`INSERT INTO run_tasks (id, subject, project_id, job_id, repo, prompt, agent, state, created_at, updated_at)++ VALUES ($1, $2, $3, 'job_01J8Z3Q4R5S6T7V8W9X0Y1Z2J98', 'https://github.com/a/b', 'an earlier task', 'claude-code', 'succeeded', 1, 2)`,++ ['tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2T90', ada.subject, project]);++++ const CAPS = ['codes.task.start', 'codes.task.read', 'codes.task.cancel'];++ const actor = t.network.mintService('svc:actor', 'openvibe.codes', CAPS);++ const startTask = async (prompt = 'Fix the parser and add a test for it.') => {++ const res = await t.get('/api/v1/tasks', {++ method: 'POST', as: ada, json: { repo: 'https://github.com/octocat/Hello-World', prompt, idempotency_key: `act_${Math.random().toString(36).slice(2, 10)}` },++ headers: { Authorization: `Bearer ${actor}`, 'X-OV-Subject': ada.subject },++ });++ assert.strictEqual(res.status, 201, res.text.slice(0, 300));++ return res.json().task.id;++ };++++ /** The attempt ends with a result that carries the proof the machine collected. */++ const endAttempt = async (taskId, { state = 'succeeded', result = null } = {}) => {++ const row = await db.one('SELECT * FROM run_tasks WHERE id = $1', [taskId]);++ const job = { id: row.job_id, state, result, error: null };++ const updated = await t.ctx.tasks.applyEvent(row, { state, job });++ await t.ctx.tasks.afterEnd(updated.id);++ return await db.one('SELECT * FROM run_tasks WHERE id = $1', [taskId]);++ };++++ /** The proof a machine would collect: the tests, the pages, the diff and the review. */++ const proofOf = (items = null) => ({++ items: items || [++ { kind: 'tests', label: 'tests', ok: true, skipped: false, summary: 'only the tests the change reaches (proof)' },++ { kind: 'screenshots', label: 'screenshots', ok: true, skipped: false, summary: '2 screenshots of 1 page' },++ { kind: 'diff', label: 'diff', ok: true, skipped: false, summary: '1 file changed' },++ { kind: 'review', label: 'review', ok: true, skipped: false, family: 'deepseek', summary: 'It holds together.' },++ ],++ outcome: { state: 'done', note: 'Done with proof.' },++ });++++ const proofRows = async (taskId) => await db.many('SELECT * FROM codes_task_proof WHERE task_id = $1 ORDER BY n ASC', [taskId]);++ const proofLines = async (taskId) => await db.many("SELECT seq, kind, body FROM codes_session_lines WHERE task_id = $1 AND kind = 'proof' ORDER BY seq", [taskId]);++++ try {++ await check('a finished task keeps its proof: one row per item, in order', async () => {++ const id = await startTask();++ await endAttempt(id, { result: { ok: true, summary: 'done', proof: proofOf() } });++ const rows = await proofRows(id);++ assert.strictEqual(rows.length, 4);++ assert.deepStrictEqual(rows.map((r) => r.kind), ['tests', 'screenshots', 'diff', 'review']);++ assert.strictEqual(rows[3].family, 'deepseek');++ assert.strictEqual(rows[0].attempt_n, 1, 'the items belong to the attempt that ended');++ });++++ await check('every proof item is an event on the task\'s stream', async () => {++ const id = await startTask();++ await endAttempt(id, { result: { ok: true, proof: proofOf() } });++ const lines = await proofLines(id);++ assert.strictEqual(lines.length, 4);++ assert.strictEqual(lines[0].body.t, 'proof');++ assert.strictEqual(lines[0].body.kind, 'tests');++ assert.strictEqual(lines[3].body.family, 'deepseek');++ assert.ok(lines.every((l, i) => i === 0 || Number(l.seq) > Number(lines[i - 1].seq)), 'the lines are in order');++ });++++ await check('the outcome is in plain words: done with proof', async () => {++ const id = await startTask();++ const row = await endAttempt(id, { result: { ok: true, proof: proofOf() } });++ assert.strictEqual(row.outcome, 'done');++ assert.ok(/^Done with proof/.test(row.outcome_note), row.outcome_note);++ });++++ await check('a proof that failed is said out loud, and the task is not called done', async () => {++ const id = await startTask();++ const row = await endAttempt(id, {++ result: { ok: true, proof: proofOf([++ { kind: 'tests', label: 'tests', ok: false, skipped: false, summary: 'FAIL test/proof.test.js' },++ { kind: 'diff', label: 'diff', ok: true, skipped: false, summary: '1 file changed' },++ ]) },++ });++ assert.strictEqual(row.outcome, 'unfinished', 'the evidence wins over the agent\'s claim');++ assert.ok(/Didn't finish, with what is left/.test(row.outcome_note), row.outcome_note);++ assert.ok(/FAIL test/.test(row.outcome_note), row.outcome_note);++ });++++ await check('a task the person stopped stays stopped, whatever the proof says', async () => {++ const id = await startTask();++ const row = await endAttempt(id, {++ state: 'cancelled',++ result: { ok: false, proof: proofOf([{ kind: 'tests', label: 'tests', ok: false, skipped: false, summary: 'FAIL' }]) },++ });++ assert.strictEqual(row.outcome, 'stopped');++ });++++ await check('a task without a proof says nothing about one', async () => {++ const id = await startTask();++ const row = await endAttempt(id, { result: { ok: true, summary: 'done' } });++ assert.strictEqual((await proofRows(id)).length, 0);++ assert.ok(row.outcome_note, 'the engine\'s own note still stands');++ });++++ await check('the API answers codes.task@2 with the outcome in plain words', async () => {++ const id = await startTask();++ await endAttempt(id, { result: { ok: true, proof: proofOf() } });++ const res = await t.get(`/api/v1/tasks/${id}`, { as: ada });++ assert.strictEqual(res.status, 200, res.text.slice(0, 200));++ const doc = res.json();++ assert.strictEqual(doc.outcome.state, 'done');++ assert.ok(/^Done with proof/.test(doc.outcome.note), doc.outcome.note);++ });++++ await check('recording the same proof twice makes no second event', async () => {++ const id = await startTask();++ const row = await db.one('SELECT * FROM run_tasks WHERE id = $1', [id]);++ const job = { id: row.job_id, state: 'succeeded', result: { ok: true, proof: proofOf() } };++ await t.ctx.tasks.applyEvent(row, { state: 'succeeded', job });++ await t.ctx.tasks.afterEnd(row.id);++ await t.ctx.tasks.applyEvent(row, { state: 'succeeded', job });++ await t.ctx.tasks.afterEnd(row.id);++ assert.strictEqual((await proofRows(id)).length, 4);++ assert.ok((await proofLines(id)).length <= 8, 'the lines do not pile up');++ });++++ await check('the task page shows the proof, each item with what it found', async () => {++ const id = await startTask();++ await endAttempt(id, { result: { ok: true, proof: proofOf() } });++ const res = await t.get(`/run/tasks/${id}`, { as: ada });++ assert.strictEqual(res.status, 200, res.text.slice(0, 200));++ assert.ok(res.text.includes('data-proof'), 'the page has the proof section');++ assert.ok(res.text.includes('data-proof-kind="tests"'), 'the tests are listed');++ assert.ok(res.text.includes('data-proof-kind="review"'), 'the review is listed');++ assert.ok(res.text.includes('deepseek'), 'the reviewer\'s family is shown');++ });++++ console.log('proof: the items are kept, each one is an event, and the outcome is in plain words');++ } catch (e) {++ console.error(e);++ process.exit(1);++ }++ done();++})();
@@ -31,7 +31,8 @@
@@ -31,7 +31,8 @@
"Improve OpenVibe (/improve): a guided run from repository choice to a task on your machine, plus the contributor path",
"Improve OpenVibe (/improve): a guided run from repository choice to a task on your machine, plus the contributor path",
"The community documents (code of conduct, contributing, contributor ladder, moderation) rendered from this repository",
"The community documents (code of conduct, contributing, contributor ladder, moderation) rendered from this repository",
"Permanent redirects for every former console address (301 for reads, 308 for writes) to openvibe.services",
"Permanent redirects for every former console address (301 for reads, 308 for writes) to openvibe.services",
- "robots.txt, sitemap.xml, llms.txt and llms-full.txt; per-actor and per-address limits; truthful readiness"
+ "robots.txt, sitemap.xml, llms.txt and llms-full.txt; per-actor and per-address limits; truthful readiness",
+ "Proof of a finished task (engine/proof.js, engine/review.js): the repo's own tests, the pages the diff touches (phone and desktop, headless Chrome), the diff, and a second model's review of another family; kept in codes_task_proof, one event per item, and the task's outcome in plain words"
],
],
"notYet": [
"notYet": [
"A hosted runner (tasks on OpenVibe.Run workers for people who do not run agents themselves) with budgets and metering",
"A hosted runner (tasks on OpenVibe.Run workers for people who do not run agents themselves) with budgets and metering",
@@ -51,7 +52,8 @@
@@ -51,7 +52,8 @@
"community documents",
"community documents",
"redirects for the moved developer console",
"redirects for the moved developer console",
"run a coding task on your own machine from any browser (/run; openvibe-codes job through OpenVibe Run and the person's own OpenVibe Node)",
"run a coding task on your own machine from any browser (/run; openvibe-codes job through OpenVibe Run and the person's own OpenVibe Node)",
- "user JSON API for signed-in people to list, start, read and cancel their own tasks"
+ "user JSON API for signed-in people to list, start, read and cancel their own tasks",
+ "proof of a finished task: its tests, screenshots, diff and a second-model review, and the outcome in plain words"
],
],
"updated": "2026-10-30"
"updated": "2026-10-30"
}
}
@@ -10,6 +10,7 @@ const path = require('path');
@@ -10,6 +10,7 @@ const path = require('path');
const { createHarness } = require('../harness');
const { createHarness } = require('../harness');
const { runJob, resultWriter, jobEnv } = require('../harness/job');
const { runJob, resultWriter, jobEnv } = require('../harness/job');
const { task } = require('./task');
const { task } = require('./task');
+const { createProofAdapters } = require('../harness/proof');
const { version: VERSION } = require('../package.json');
const { version: VERSION } = require('../package.json');
const { probe: probeLogins } = require('../engine/logins');
const { probe: probeLogins } = require('../engine/logins');
@@ -164,7 +165,16 @@ async function main() {
@@ -164,7 +165,16 @@ async function main() {
let wanted = {};
let wanted = {};
try { wanted = jobEnv(JSON.parse(input)); } catch { wanted = {}; }
try { wanted = jobEnv(JSON.parse(input)); } catch { wanted = {}; }
const jobHarness = createHarness({ env: { ...process.env, ...wanted } });
const jobHarness = createHarness({ env: { ...process.env, ...wanted } });
- await runJob({ input, harness: jobHarness, emit: out, writeResult: resultWriter(out) });
+ // The proof adapters: the repo's own tests, its pages in a browser, its diff, and the second model. The review
+ // spends the same run token the attempt already carries, so it names the same person.
+ const proof = createProofAdapters({
+ cwd: process.cwd(),
+ env: { ...process.env, ...wanted },
+ jobEnv: wanted,
+ // The models this machine can offer a reviewer: the catalog's own.
+ models: (jobHarness.agents() || []).flatMap((a) => (a.agents || []).map((x) => x.model)).filter(Boolean),
+ });
+ await runJob({ input, harness: jobHarness, emit: out, writeResult: resultWriter(out), proof });
return 0;
return 0;
}
}
if (cmd === 'runs') {
if (cmd === 'runs') {
@@ -17,6 +17,8 @@
@@ -17,6 +17,8 @@
* engine.joblines the lines a task's page shows while it runs
* engine.joblines the lines a task's page shows while it runs
* engine.logins what this machine has: each CLI, signed in or not, and its limit windows
* engine.logins what this machine has: each CLI, signed in or not, and its limit windows
* engine.redact the secrets that never reach an event stream
* engine.redact the secrets that never reach an event stream
+ * engine.proof the proof of a finished attempt: its tests, its pages, its diff, and a second opinion
+ * engine.review the second opinion itself: a model of another family, through OpenVibe.AI
*
*
* The modules do not depend on each other in a cycle, and none of them reads the portal's log folder.
* The modules do not depend on each other in a cycle, and none of them reads the portal's log folder.
*/
*/
@@ -35,6 +37,8 @@ const repomap = require('./repomap');
@@ -35,6 +37,8 @@ const repomap = require('./repomap');
const joblines = require('./joblines');
const joblines = require('./joblines');
const logins = require('./logins');
const logins = require('./logins');
const redact = require('./redact');
const redact = require('./redact');
+const proof = require('./proof');
+const review = require('./review');
module.exports = {
module.exports = {
router,
router,
@@ -51,6 +55,8 @@ module.exports = {
@@ -51,6 +55,8 @@ module.exports = {
joblines,
joblines,
logins,
logins,
redact,
redact,
+ proof,
+ review,
// The names a caller usually wants, in one place.
// The names a caller usually wants, in one place.
classify: router.classify,
classify: router.classify,
pick: router.pick,
pick: router.pick,
new file mode 100644
new file mode 100644
@@ -0,0 +1,297 @@
@@ -0,0 +1,297 @@
+'use strict';
+
+/**
+ * Proof, not promises: what a finished attempt has to show.
+ *
+ * When an attempt says it is done, this asks for the evidence: the tests the change reaches, the pages it touched
+ * (phone and desktop), the diff, and a second model's opinion. Everything it touches is an injected adapter, so the
+ * order is the same in a test with fakes as on a machine, and a step that cannot run says so instead of being
+ * quietly left out.
+ *
+ * collectProof({ cwd, files, adapters, author, emit }) the proof: its items, and the outcome in plain words
+ * testPlan({ pkg, files, env }) the repo's own test command, and why it is that one
+ * pageFiles(files) the changed files that are pages
+ * routesOf(pages) the routes those pages are served at
+ * outcomeOf(items) 'Done with proof' / "Didn't finish, with what is left"
+ *
+ * The four steps, in order:
+ *
+ * tests the repo's own runner, narrowed to the tests the change reaches. The whole suite runs only when
+ * the change touches what every test shares and the owner's guard allows it (CODES_PROOF_FULL_SUITE=1):
+ * a machine may not spend an hour of its owner's battery on every task.
+ * screenshots phone (390) and desktop (1440) of the pages the diff touches, because a page that overflows is a
+ * bug the diff alone never shows.
+ * diff the stats: what changed, how many lines, which files.
+ * review a model of another family, through OpenVibe.AI. A review by the author's own family is refused:
+ * that would be a second reading of the same mind, not a second opinion.
+ *
+ * The outcome is the three plain words a person reads: Done with proof, Didn't finish (with what is left), or
+ * Stopped by you.
+ */
+
+const { pickReviewer, authorFamily: familyOfAuthor } = require('./review');
+
+/** The changed files that are pages: the site's own files, and anything served as HTML or CSS. */
+const PAGE_FILE = /^(?:public\/|server\/render\/|server\/http\/)|\.(?:html?|css)$/i;
+
+/** The files that make every test matter: the shared ones. A change here runs the whole suite (when allowed). */
+const SHARED_FILE = /^(?:package\.json|package-lock\.json|engine\/index\.js|server\/app\.js|server\/db\.js|server\/config\.js)$/;
+
+/** The kinds of proof item, in the order they are collected. */
+const KINDS = ['tests', 'screenshots', 'diff', 'review'];
+
+/** The plain words for a finished task. */
+const OUTCOME = {
+ done: 'Done with proof',
+ unfinished: "Didn't finish",
+ stopped: 'Stopped by you',
+};
+
+/**
+ * The changed files that are pages.
+ *
+ * @param {string[]} files the changed files
+ * @returns {string[]} the pages among them
+ */
+function pageFiles(files) {
+ return (Array.isArray(files) ? files : []).filter((f) => typeof f === 'string' && PAGE_FILE.test(f));
+}
+
+/**
+ * The words the repo's own test runner filters by, for the changed files: a test file is named after what it
+ * tests ('test/engine-proof.test.js' for 'engine/proof.js'), so the name without its extension finds it.
+ *
+ * @param {string[]} files the changed files
+ * @returns {string[]} the filter words, unique, without the words every file shares
+ */
+function testWords(files) {
+ const skip = new Set(['index', 'main', 'app', 'server', 'public', 'src', 'lib', 'test', 'tests', 'util', 'utils', 'helper', 'helpers', 'config']);
+ const words = new Set();
+ for (const f of Array.isArray(files) ? files : []) {
+ if (typeof f !== 'string') continue;
+ const name = String(f).split('/').pop().replace(/\.[^.]+$/, '');
+ for (const part of name.split(/[-_.\s]+/)) {
+ const w = part.toLowerCase();
+ if (w.length >= 3 && !skip.has(w)) words.add(w);
+ }
+ }
+ return [...words].slice(0, 8);
+}
+
+/**
+ * The file a `node <file>` test script runs, or null.
+ *
+ * 'npm test' would run it with one more process in the way, and a machine's npm may be a guard that cannot tell a
+ * narrowed run from a whole one. Running the script itself is the same runner, with the words the plan chose.
+ */
+function nodeTestScript(script) {
+ const m = /^node\s+([^\s-][^\s]*)$/.exec(String(script || '').trim());
+ return m ? m[1] : null;
+}
+
+/**
+ * The repo's own test command, and how narrow it is.
+ *
+ * The runner is the repo's: 'npm test' when package.json has it (or the file it runs, when that is `node <file>`),
+ * then pytest, go and cargo. The suite is narrowed to the tests the change reaches; the whole suite runs only when
+ * the change touches what every test shares and the owner's guard allows it (CODES_PROOF_FULL_SUITE=1).
+ *
+ * @param {object} what
+ * @param {object} what.pkg the repo's package.json (or null)
+ * @param {string[]} what.files the changed files
+ * @param {object} what.env the environment
+ * @returns {{ command: string, args: string[], mode: string, reason: string }} the plan
+ */
+function testPlan({ pkg = null, files = [], env = {} } = {}) {
+ const scripts = pkg && pkg.scripts ? pkg.scripts : {};
+ const test = typeof scripts.test === 'string' ? scripts.test : null;
+ const changed = Array.isArray(files) ? files : [];
+ const shared = changed.some((f) => SHARED_FILE.test(String(f || '')));
+ // The guard: the whole suite runs only when the owner allows it. CODES_PROOF_FULL_SUITE is the proof step's own
+ // name for it; OV_ALLOW_SUITE is the fleet's (the harness refuses a whole suite without it too).
+ const allowFull = /^(1|true|yes)$/i.test(String(env.CODES_PROOF_FULL_SUITE || env.OV_ALLOW_SUITE || ''));
+ const words = testWords(changed);
+
+ if (test) {
+ const direct = nodeTestScript(test);
+ const base = direct ? { command: 'node', args: [direct] } : { command: 'npm', args: ['test'] };
+ const narrowed = direct ? [direct, ...words] : ['test', '--', ...words];
+ if (shared && allowFull) return { ...base, mode: 'full', reason: 'the change touches what every test shares, and the full suite is allowed' };
+ if (!shared) {
+ if (!words.length) return { ...base, mode: 'targeted', reason: 'no test name matches the changed files, so the suite runs as it is' };
+ return { command: base.command, args: narrowed, mode: 'targeted', reason: 'only the tests the change reaches (' + words.join(', ') + ')' };
+ }
+ return { command: base.command, args: narrowed, mode: 'targeted', reason: 'the change touches what every test shares, but the full suite is not allowed (CODES_PROOF_FULL_SUITE=1)' };
+ }
+ if (pkg && (pkg.go || (pkg.dependencies && pkg.dependencies.go))) return { command: 'go', args: ['test', './...'], mode: 'full', reason: "the repo's own Go suite" };
+ if (pkg && (pkg.pytest || (pkg.dependencies && pkg.dependencies.pytest))) return { command: 'pytest', args: [], mode: 'full', reason: "the repo's own Python suite" };
+ if (pkg && pkg.rust) return { command: 'cargo', args: ['test'], mode: 'full', reason: "the repo's own Rust suite" };
+ return { command: '', args: [], mode: 'none', reason: 'the repo has no test command of its own' };
+}
+
+/**
+ * The outcome in plain words: done with proof, or what is left.
+ *
+ * @param {object[]} items the proof items
+ * @returns {{ state: string, note: string }} the outcome
+ */
+function proofNote(items) {
+ const list = Array.isArray(items) ? items : [];
+ const failed = list.filter((i) => i && i.ok === false);
+ const passed = list.filter((i) => i && i.ok === true && i.skipped !== true);
+ const skipped = list.filter((i) => i && i.skipped === true);
+ if (failed.length) {
+ const left = failed.map((i) => (i.label || 'a step') + ': ' + (i.summary || 'it failed')).join('; ');
+ return "Didn't finish, with what is left: " + left + '.';
+ }
+ const skippedText = skipped.length
+ ? ' (' + skipped.map((i) => (i.label || 'a step') + ' was skipped: ' + (i.summary || 'not available here')).join('; ') + ')'
+ : '';
+ if (!passed.length) return 'Done.' + skippedText;
+ return 'Done with proof: ' + passed.map((i) => i.label).join(', ') + '.' + skippedText;
+}
+
+function outcomeOf(items) {
+ const list = Array.isArray(items) ? items : [];
+ const failed = list.filter((i) => i && i.ok === false);
+ return { state: failed.length ? 'unfinished' : 'done', note: proofNote(list) };
+}
+
+/**
+ * The routes the changed pages are served at: a public/index.html is the home, a public/a/b.html is /a/b.
+ *
+ * @param {string[]} pages the changed page files
+ * @returns {string[]} the routes
+ */
+function routesOf(pages) {
+ const out = [];
+ for (const p of Array.isArray(pages) ? pages : []) {
+ if (!String(p).startsWith('public/')) continue;
+ let route = String(p).slice('public/'.length).replace(/\.(html?|css)$/i, '');
+ route = route.replace(/^(.*\/)?index$/i, ''); // public/index.html is the home, public/a/index.html is /a
+ if (route) out.push('/' + route);
+ else if (/^public\/index\.html?$/i.test(String(p))) out.push('/');
+ }
+ return [...new Set(out.length ? out : ['/'])];
+}
+
+/**
+ * Collect the proof of one finished attempt.
+ *
+ * @param {object} what
+ * @param {string} what.cwd the repo the change is in
+ * @param {string[]} [what.files] the changed files
+ * @param {object} what.adapters { run, diff, screenshot, review, models, env }
+ * @param {object} [what.author] { agent, model } the author of the change
+ * @param {function} [what.emit] called with every item as it is collected
+ * @returns {Promise<{ items: object[], outcome: object, review: object|null }>} the proof
+ */
+async function collectProof({ cwd, files = [], adapters = {}, author = null, emit = null } = {}) {
+ if (!cwd) throw new TypeError('proof needs the repo it is about');
+ const run = adapters.run;
+ if (typeof run !== 'function') throw new TypeError('proof needs a run adapter');
+ const items = [];
+ const say = async (item) => { items.push(item); if (typeof emit === 'function') await emit(item); };
+
+ // The diff: what changed, in numbers and in words. Every later step reads it.
+ const diff = typeof adapters.diff === 'function' ? await adapters.diff({ cwd }) : null;
+ const changed = Array.isArray(diff && diff.files) ? diff.files : (Array.isArray(files) ? files : []);
+ const stats = diff && diff.stats ? String(diff.stats) : '';
+ const patch = diff && diff.patch ? String(diff.patch) : '';
+
+ // 1. the tests the change reaches.
+ let pkg = null;
+ const pkgRead = await run(['node', '-e', "try{process.stdout.write(require('fs').readFileSync('package.json','utf8'))}catch(e){}"], { cwd });
+ if (pkgRead && pkgRead.code === 0 && pkgRead.output) { try { pkg = JSON.parse(pkgRead.output); } catch { pkg = null; } }
+ const plan = testPlan({ pkg, files: changed, env: adapters.env || {} });
+ if (!plan.command) {
+ await say({ kind: 'tests', label: 'tests', ok: true, skipped: true, summary: plan.reason, mode: 'none' });
+ } else {
+ const result = await run([plan.command, ...plan.args], { cwd, timeoutMs: adapters.testTimeoutMs || 600000 });
+ const output = String((result && result.output) || '');
+ const ok = !!result && result.code === 0;
+ await say({
+ kind: 'tests', label: 'tests', ok, skipped: false, mode: plan.mode, reason: plan.reason,
+ command: [plan.command, ...plan.args].join(' '),
+ summary: ok ? plan.reason : (output.trim().split('\n').slice(-6).join(' ') || 'the tests failed (exit ' + result.code + ')'),
+ });
+ }
+
+ // 2. the pages it touched, at the widths a person reads them.
+ const pages = pageFiles(changed);
+ if (!pages.length) {
+ await say({ kind: 'screenshots', label: 'screenshots', ok: true, skipped: true, summary: 'the change touches no page' });
+ } else if (typeof adapters.screenshot !== 'function') {
+ await say({ kind: 'screenshots', label: 'screenshots', ok: true, skipped: true, summary: 'this machine cannot take screenshots' });
+ } else {
+ const shots = [];
+ for (const width of [390, 1440]) {
+ const shot = await adapters.screenshot({ cwd, width, files: pages, routes: routesOf(pages), pkg });
+ shots.push({ width, ...(shot || {}) });
+ }
+ const made = shots.filter((s) => s && !s.skipped);
+ const bad = made.filter((s) => s.ok === false);
+ // A machine with nothing to shoot with (no browser, a site that did not start) says so: it is a skip, and the
+ // reason is the adapter's own words, not a clean bill of health.
+ const why = shots.find((s) => s && s.skipped);
+ await say({
+ kind: 'screenshots', label: 'screenshots', ok: !bad.length, skipped: !made.length,
+ summary: bad.length
+ ? bad.map((s) => s.width + 'px: ' + (s.summary || 'the page did not render')).join('; ')
+ : (made.length ? made.length + ' screenshots of ' + pages.length + ' page' + (pages.length === 1 ? '' : 's')
+ : (why && why.summary) || 'no page was there to shoot'),
+ shots,
+ });
+ }
+
+ // 3. the diff itself.
+ await say({
+ kind: 'diff', label: 'diff', ok: true, skipped: false,
+ summary: stats || (changed.length ? changed.length + ' file' + (changed.length === 1 ? '' : 's') + ' changed' : 'nothing changed'),
+ files: changed, stats, patch: patch.slice(0, 262144),
+ });
+
+ // 4. the second opinion, from a model of another family.
+ const family = familyOfAuthor(author || {});
+ let verdict = null;
+ if (typeof adapters.review !== 'function') {
+ await say({ kind: 'review', label: 'review', ok: true, skipped: true, summary: 'no reviewer is connected here' });
+ } else {
+ const models = Array.isArray(adapters.models) ? adapters.models : [];
+ const chosen = pickReviewer({ models, author: family });
+ if (!chosen) {
+ // Nobody reviewed this: not a refusal, and not a second opinion either. It is said out loud as a skip.
+ await say({ kind: 'review', label: 'review', ok: true, skipped: true, summary: 'no model of another family is connected, so no second opinion was asked for' });
+ } else {
+ try {
+ const tests = items.find((i) => i.kind === 'tests');
+ verdict = await adapters.review({
+ patch, summary: (diff && diff.summary) || '',
+ tests: tests ? tests.summary : null,
+ model: chosen.model, family: chosen.family,
+ });
+ } catch (e) {
+ // A reviewer that is not connected is a skip, not a failure: the change was not refused, nobody looked.
+ if (e && (e.code === 'review.no_token' || e.code === 'review.no_model')) {
+ await say({ kind: 'review', label: 'review', ok: true, skipped: true, summary: e.message });
+ return { items, outcome: outcomeOf(items), review: null };
+ }
+ verdict = { family: chosen.family, model: chosen.model, verdict: 'changes_requested', summary: 'the review failed: ' + e.message, error: true };
+ }
+ if (!verdict || !verdict.family || verdict.family === family) {
+ await say({ kind: 'review', label: 'review', ok: false, skipped: false, family: verdict && verdict.family, summary: "the review came from the author's own family, so it is not a second opinion" });
+ verdict = null;
+ } else {
+ await say({
+ kind: 'review', label: 'review', ok: verdict.verdict === 'approved', skipped: false,
+ family: verdict.family, model: verdict.model,
+ summary: verdict.summary || (verdict.verdict === 'approved' ? 'approved' : 'changes requested'),
+ });
+ }
+ }
+ }
+
+ return { items, outcome: outcomeOf(items), review: verdict };
+}
+
+module.exports = { collectProof, testPlan, testWords, pageFiles, routesOf, outcomeOf, proofNote, OUTCOME, KINDS, PAGE_FILE, SHARED_FILE };
new file mode 100644
new file mode 100644
@@ -0,0 +1,171 @@
@@ -0,0 +1,171 @@
+'use strict';
+
+/**
+ * The second opinion on a change: a model of another family than the one that wrote it.
+ *
+ * "Another family" is the rule that makes the second opinion worth reading. A model of the same family tends to
+ * repeat its own reasoning, so the family is part of the verdict: a review by the author's own family is not
+ * accepted, and the collector says so instead of pretending a second look happened.
+ *
+ * The call goes to OpenVibe.AI, the one place that holds the person's keys (stage C3): POST /v1/chat/completions with
+ * the attempt's run token, or /v1/messages for a provider that speaks that dialect. The token is passed in, never read
+ * from here: this module never sees a secret and never logs one.
+ *
+ * familyOf(model) the family a model belongs to ('anthropic', 'openai', 'deepseek', …)
+ * authorFamily({ agent, model }) the family that wrote the change
+ * pickReviewer({ models, author }) a model of another family, or null when there is none
+ * reviewWithAi({ … }) the review as { family, verdict, summary }
+ *
+ * The verdict is 'approved' or 'changes_requested'. The model is asked for JSON; a model that answers in prose is
+ * read by its words ('approved', 'looks good' → approved; 'changes', 'needs work' → changes_requested), because a
+ * review that cannot be read is not a review.
+ */
+
+/** The families this knows, by the model's own name. */
+const FAMILIES = [
+ { family: 'anthropic', re: /claude/i },
+ { family: 'openai', re: /\bgpt-|\bo[134]\b|codex/i },
+ { family: 'google', re: /gemini/i },
+ { family: 'deepseek', re: /deepseek/i },
+ { family: 'minimax', re: /minimax|abab/i },
+ { family: 'qwen', re: /qwen/i },
+ { family: 'moonshot', re: /kimi|moonshot/i },
+];
+
+/** The families Codes' own harnesses belong to, by the harness id. */
+const HARNESS_FAMILY = {
+ 'claude-code': 'anthropic',
+ codex: 'openai',
+ 'command-code': 'deepseek',
+ opencode: 'qwen',
+ deepseek: 'deepseek',
+ aider: 'openai',
+ 'openvibe-ai': 'openai',
+ 'openai-compatible': 'openai',
+};
+
+/**
+ * The family a model belongs to, or null when its name says nothing.
+ *
+ * @param {string} model the model's name
+ * @returns {string|null} the family
+ */
+function familyOf(model) {
+ const name = String(model || '').trim();
+ if (!name) return null;
+ for (const f of FAMILIES) if (f.re.test(name)) return f.family;
+ return null;
+}
+
+/**
+ * The family that wrote the change: the model's own name first, the harness second.
+ *
+ * @param {object} what { agent, model }
+ * @returns {string|null} the family
+ */
+function authorFamily({ agent = null, model = null } = {}) {
+ return familyOf(model) || HARNESS_FAMILY[String(agent || '')] || null;
+}
+
+/**
+ * A model of another family, for the second opinion. The first one that is not the author's family wins.
+ *
+ * @param {object} what { models, author }
+ * @returns {{ model: string, family: string }|null} the reviewer, or null when every model is the author's family
+ */
+function pickReviewer({ models = [], author = null } = {}) {
+ const list = Array.isArray(models) ? models : [];
+ for (const m of list) {
+ const name = typeof m === 'string' ? m : (m && m.model) || null;
+ if (!name) continue;
+ const family = familyOf(name);
+ if (!family || (author && family === author)) continue;
+ return { model: name, family };
+ }
+ return null;
+}
+
+/** The prompt the reviewer gets: the change, the tests it claims to pass, and what is being asked of it. */
+function reviewPrompt({ patch = '', summary = '', tests = null } = {}) {
+ return [
+ 'You are reviewing a change to a repository. Answer only with JSON:',
+ '{"verdict":"approved"|"changes_requested","summary":"one paragraph, what you checked and what you found"}.',
+ 'Answer "approved" when the change does what it says and holds together; "changes_requested" when it is wrong,',
+ 'unsafe, or leaves the work unfinished. Be concrete: name the file and the line.',
+ '',
+ summary ? 'What the change was asked to do:\n' + String(summary).slice(0, 4000) : '',
+ tests ? 'The tests the author says pass:\n' + String(tests).slice(0, 2000) : '',
+ '',
+ 'The diff:\n' + String(patch || '').slice(0, 120000),
+ ].filter(Boolean).join('\n');
+}
+
+/** The answer as JSON, or as the words in it. */
+function readVerdict(text) {
+ const raw = String(text || '').trim();
+ if (!raw) return { verdict: 'changes_requested', summary: 'the reviewer said nothing' };
+ const start = raw.indexOf('{');
+ const end = raw.lastIndexOf('}');
+ if (start >= 0 && end > start) {
+ try {
+ const doc = JSON.parse(raw.slice(start, end + 1));
+ const verdict = String(doc.verdict || '').toLowerCase();
+ if (/approv|looks good|lgtm/.test(verdict)) return { verdict: 'approved', summary: String(doc.summary || raw).slice(0, 4000) };
+ if (/change|reject|not good/.test(verdict)) return { verdict: 'changes_requested', summary: String(doc.summary || raw).slice(0, 4000) };
+ if (doc.summary) return { verdict: 'changes_requested', summary: String(doc.summary).slice(0, 4000) };
+ } catch { /* not JSON: read the words */ }
+ }
+ if (/\bapprov|looks good|looks fine|lgtm|no changes\b/i.test(raw)) return { verdict: 'approved', summary: raw.slice(0, 4000) };
+ return { verdict: 'changes_requested', summary: raw.slice(0, 4000) };
+}
+
+/**
+ * The review itself: one request to OpenVibe.AI, read as a verdict.
+ *
+ * @param {object} what
+ * @param {string} what.baseUrl OpenVibe.AI's OpenAI-compatible root
+ * @param {string} what.token the task's token (never logged)
+ * @param {string} what.model the reviewer's model
+ * @param {string} what.family the reviewer's family
+ * @param {string} [what.patch] the diff
+ * @param {string} [what.summary] what the change was for
+ * @param {function} [what.fetchImpl] fetch, for a test
+ * @returns {Promise<{ family: string, verdict: string, summary: string }>} the verdict
+ */
+async function reviewWithAi({
+ baseUrl = 'https://ai.openvibe.services/v1',
+ token,
+ model,
+ family = null,
+ patch = '',
+ summary = '',
+ tests = null,
+ fetchImpl = globalThis.fetch,
+} = {}) {
+ if (!token) throw new Error('the review needs the task\'s token');
+ if (!model) throw new Error('the review needs a model');
+ const root = String(baseUrl).replace(/\/+$/, '');
+ const prompt = reviewPrompt({ patch, summary, tests });
+ const headers = { 'content-type': 'application/json', authorization: `Bearer ${token}` };
+ let res = await fetchImpl(`${root}/chat/completions`, {
+ method: 'POST', headers, body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }] }),
+ });
+ if (res && res.status === 404) {
+ // A provider that speaks the messages dialect instead.
+ res = await fetchImpl(`${root}/messages`, {
+ method: 'POST', headers, body: JSON.stringify({ model, max_tokens: 1200, messages: [{ role: 'user', content: prompt }] }),
+ });
+ }
+ if (!res || !res.ok) throw new Error(`OpenVibe.AI refused the review (HTTP ${res && res.status})`);
+ const body = await res.json().catch(() => null);
+ const text = body && body.choices && body.choices[0] && body.choices[0].message && body.choices[0].message.content
+ ? body.choices[0].message.content
+ : (body && body.content && Array.isArray(body.content) ? body.content.map((c) => c && c.text).filter(Boolean).join(' ') : '');
+ const verdict = readVerdict(text);
+ return { family: family || familyOf(model), model, verdict: verdict.verdict, summary: verdict.summary };
+}
+
+module.exports = {
+ FAMILIES, HARNESS_FAMILY,
+ familyOf, authorFamily, pickReviewer, reviewPrompt, readVerdict, reviewWithAi,
+};
@@ -17,6 +17,7 @@ const fs = require('fs');
@@ -17,6 +17,7 @@ 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 JOB_PROTOCOL = '1.0.0'; // the job's contract with the Node (stdin, stdout lines, fd 3): unchanged by 1.1's lines
const JOB_PROTOCOL = '1.0.0'; // the job's contract with the Node (stdin, stdout lines, fd 3): unchanged by 1.1's lines
const LINES = '1.1'; // 1.1: lines carry step ids, inputs, results, progress, helpers; the result keeps `log`
const LINES = '1.1'; // 1.1: lines carry step ids, inputs, results, progress, helpers; the result keeps `log`
@@ -276,7 +277,7 @@ async function pushBranch(dir, job, { env, fetchImpl, base }) {
@@ -276,7 +277,7 @@ async function pushBranch(dir, job, { env, fetchImpl, base }) {
* Run one job. `harness` is createHarness(); `emit(line)` prints a line; `writeResult(doc)` hands the result over.
* Run one job. `harness` is createHarness(); `emit(line)` prints a line; `writeResult(doc)` hands the result over.
* → the result document.
* → the result document.
*/
*/
-async function runJob({ input, harness, emit, writeResult, env = process.env, tmp = os.tmpdir(), fetchImpl = globalThis.fetch }) {
+async function runJob({ input, harness, emit, writeResult, env = process.env, tmp = os.tmpdir(), fetchImpl = globalThis.fetch, proof = null }) {
const started = Date.now();
const started = Date.now();
const log = [];
const log = [];
const say = (doc) => { if (doc && doc.t && doc.t !== 'result_doc') log.push(doc); emit(JSON.stringify(doc)); };
const say = (doc) => { if (doc && doc.t && doc.t !== 'result_doc') log.push(doc); emit(JSON.stringify(doc)); };
@@ -317,6 +318,24 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
@@ -317,6 +318,24 @@ async function runJob({ input, harness, emit, writeResult, env = process.env, tm
result.cost_usd = last && last.total_cost_usd ? last.total_cost_usd : 0;
result.cost_usd = last && last.total_cost_usd ? last.total_cost_usd : 0;
Object.assign(result, await changes(work));
Object.assign(result, await changes(work));
say({ t: 'step', text: result.files.length ? `${result.files.length} file${result.files.length === 1 ? '' : 's'} changed` : 'No files changed' });
say({ t: 'step', text: result.files.length ? `${result.files.length} file${result.files.length === 1 ? '' : 's'} changed` : '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.
+ if (proof) {
+ say({ t: 'step', text: 'Collecting the proof' });
+ try {
+ const collected = await collectProof({
+ cwd: work,
+ files: result.files.map((f) => f.path),
+ adapters: proof,
+ author: { agent: job.agent, model: job.model },
+ emit: (item) => say({ t: 'proof', kind: item.kind, label: item.label, ok: item.ok, skipped: !!item.skipped, summary: item.summary, family: item.family || null }),
+ });
+ result.proof = { items: collected.items.map((i) => ({ kind: i.kind, label: i.label, ok: i.ok, skipped: !!i.skipped, summary: i.summary, family: i.family || null, detail: i.shots ? { shots: i.shots } : (i.kind === 'diff' ? { files: i.files, stats: i.stats } : null) })), outcome: collected.outcome };
+ } catch (e) {
+ say({ t: 'note', level: 'warning', text: `The proof could not be collected: ${scrub(e.message)}` });
+ result.proof = { items: [], outcome: null };
+ }
+ }
if (job.push && result.ok && result.files.length) {
if (job.push && result.ok && result.files.length) {
say({ t: 'step', text: 'Pushing a new branch' });
say({ t: 'step', text: 'Pushing a new branch' });
try {
try {
new file mode 100644
new file mode 100644
@@ -0,0 +1,323 @@
@@ -0,0 +1,323 @@
+'use strict';
+
+/**
+ * The proof adapters on a machine: the repo's own tests, its pages in a browser, its diff, and the second model.
+ *
+ * The engine (engine/proof.js) says what to do; this file does it. Every effect is a separate adapter so a test can
+ * replace one of them: the tests run through the repo's own runner, the pages are shot with headless Chrome at a
+ * phone's and a desktop's width, and the review goes to OpenVibe.AI.
+ *
+ * createProofAdapters({ cwd, env, fetchImpl, chromePath, playwrightPath })
+ * run(argv, { cwd }) a command in the repo, with its output
+ * diff() the diff and its stats, against HEAD
+ * screenshot({ width, … }) one page at one width, with the overflow and console errors
+ * review({ patch, … }) the second opinion (OpenVibe.AI)
+ * models the models a reviewer may be chosen from
+ *
+ * The review is behind an interface (engine/review.js). The token that names the person is the run token the attempt
+ * already carries (stage C3): it comes in the job's own input, is never read from the machine's other keys, and when
+ * there is none the adapter says so instead of pretending a second opinion happened.
+ */
+
+const fs = require('fs');
+const net = require('net');
+const path = require('path');
+const { execFile, spawn } = require('child_process');
+const { reviewWithAi, familyOf } = require('../engine/review');
+
+const MAX_OUTPUT = 8000;
+const MAX_PATCH = 200000;
+
+/** Where Playwright may live: the project's own, then what the owner names. */
+const PLAYWRIGHT_PATHS = [
+ null, // the repo's own node_modules
+ '/home/workstation/openvibe/work/OpenVibe.Games/node_modules/playwright',
+];
+
+/** Playwright, or null when this machine has none. */
+function findPlaywright(env = {}) {
+ const named = env.CODES_PLAYWRIGHT_PATH || env.PLAYWRIGHT_PATH;
+ const tries = named ? [named, ...PLAYWRIGHT_PATHS] : PLAYWRIGHT_PATHS;
+ for (const p of tries) {
+ try {
+ const mod = p ? require(p) : require('playwright');
+ if (mod && mod.chromium) return mod;
+ } catch { /* try the next one */ }
+ }
+ return null;
+}
+
+/** A command in the repo: no shell, a timeout, and the output it printed. */
+function commandRunner({ env = process.env } = {}) {
+ return async function run(argv, { cwd, timeoutMs = 120000 } = {}) {
+ if (!Array.isArray(argv) || !argv.length) return { code: 1, output: 'no command' };
+ return await new Promise((resolve) => {
+ const child = execFile(argv[0], argv.slice(1), {
+ cwd, timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024,
+ env: { ...env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo' },
+ }, (err, stdout, stderr) => {
+ const code = err && typeof err.code === 'number' ? err.code : (err ? 1 : 0);
+ const out = String(stdout || '') + (stderr ? '\n' + String(stderr) : '');
+ resolve({ code, output: out.length > MAX_OUTPUT ? out.slice(-MAX_OUTPUT) : out });
+ });
+ });
+ };
+}
+
+/** The diff against HEAD: the stats a person reads, and the patch a reviewer reads. */
+function diffAdapter(run) {
+ return async function diff({ cwd } = {}) {
+ const where = cwd || run.cwd;
+ const stats = await run(['git', 'diff', '--stat', 'HEAD'], { cwd: where });
+ const names = await run(['git', 'diff', '--name-only', 'HEAD'], { cwd: where });
+ const patch = await run(['git', 'diff', '--no-color', 'HEAD'], { cwd: where });
+ const files = String(names.output || '').split('\n').map((s) => s.trim()).filter(Boolean);
+ return {
+ files,
+ stats: String(stats.output || '').trim(),
+ patch: String(patch.output || '').slice(0, MAX_PATCH),
+ summary: files.length ? files.length + ' file' + (files.length === 1 ? '' : 's') + ' changed' : 'nothing changed',
+ };
+ };
+}
+
+/**
+ * One page at one width: the screenshot, the sideways scroll and the console errors.
+ *
+ * @returns {Promise<object>} { ok, path, summary, width, errors, overflow }
+ */
+async function shootPage({ playwright, chromePath, url, width, outDir, route }) {
+ const browser = await playwright.chromium.launch({ executablePath: chromePath, args: ['--no-sandbox'] });
+ try {
+ const page = await browser.newPage({
+ viewport: { width, height: 900 }, deviceScaleFactor: 1,
+ isMobile: width < 700, hasTouch: width < 700,
+ });
+ const errors = [];
+ const consoleErrors = [];
+ page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e).slice(0, 300)));
+ // Console errors are kept, not fatal: a blocked third-party request (a frame, a theme, a font) is the network
+ // around the page, not a bug in it. An uncaught exception is the page's own, and that one fails the proof.
+ page.on('console', (m) => { if (m && m.type() === 'error') consoleErrors.push(String(m.text()).slice(0, 300)); });
+ await page.goto(url, { waitUntil: 'load', timeout: 30000 });
+ await page.waitForTimeout(600);
+ const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
+ const file = path.join(outDir, `proof-${(route || '/').replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '') || 'home'}-${width}.png`);
+ await page.screenshot({ path: file, fullPage: true });
+ const overflow = Number(scrollWidth) > width;
+ const ok = !overflow && !errors.length;
+ return {
+ ok, path: file, width, route: route || '/', errors, console_errors: consoleErrors, overflow, url,
+ final_url: typeof page.url === 'function' ? page.url() : url,
+ summary: ok ? 'the page renders clean'
+ : [overflow ? 'the page is ' + scrollWidth + 'px wide, so it scrolls sideways on a ' + width + 'px screen' : null]
+ .concat(errors.length ? ['the page threw: ' + errors.slice(0, 3).join('; ')] : []).filter(Boolean).join('; '),
+ };
+ } finally {
+ await browser.close().catch(() => {});
+ }
+}
+
+/** A port nothing is using, so the repo's site does not collide with what the person already runs. */
+function freePort() {
+ return new Promise((resolve, reject) => {
+ const probe = net.createServer();
+ probe.on('error', reject);
+ probe.listen(0, '127.0.0.1', () => {
+ const port = probe.address().port;
+ probe.close(() => resolve(port));
+ });
+ });
+}
+
+/** Wait until the site answers: a redirect or a 401 is up; a 5xx or no answer is not. */
+async function waitForSite(url, { fetchImpl = globalThis.fetch, timeoutMs = 30000, everyMs = 250 } = {}) {
+ const until = Date.now() + timeoutMs;
+ let last = null;
+ while (Date.now() < until) {
+ try {
+ const res = await fetchImpl(url, { redirect: 'manual' });
+ if (res && res.status < 500) return { ok: true, status: res.status };
+ last = res ? 'HTTP ' + res.status : 'no response';
+ } catch (e) {
+ last = e && e.message ? e.message : String(e);
+ }
+ await new Promise((r) => setTimeout(r, everyMs));
+ }
+ return { ok: false, why: last || 'no answer' };
+}
+
+/**
+ * The repo's own site, up: the URL it answers on, and how to stop it.
+ *
+ * CODES_PROOF_URL names a site that is already running (a dev server, a preview). Otherwise the repo's own start
+ * script runs on a free port in its own process group, so the group can be stopped without touching anything else
+ * the person runs. The script is `npm start` unless CODES_PROOF_START names another command.
+ *
+ * @returns {Promise<{ url: string, stop: function }>} the site
+ */
+async function startSite({ cwd, env = process.env, fetchImpl = globalThis.fetch, spawnFn = spawn, timeoutMs = 30000 } = {}) {
+ const named = String(env.CODES_PROOF_URL || '').trim();
+ if (named) {
+ const url = named.replace(/\/+$/, '');
+ const ready = await waitForSite(url, { fetchImpl, timeoutMs });
+ if (!ready.ok) {
+ const err = new Error('the site at ' + url + ' did not answer: ' + ready.why);
+ err.code = 'site.not_ready';
+ throw err;
+ }
+ return { url, stop: async () => {} };
+ }
+ const port = await freePort();
+ const url = 'http://127.0.0.1:' + port;
+ const [command, ...args] = String(env.CODES_PROOF_START || 'npm start').split(/\s+/).filter(Boolean);
+ const child = spawnFn(command, args, {
+ cwd, env: { ...env, PORT: String(port), HOST: '127.0.0.1' }, stdio: 'ignore', detached: true,
+ });
+ const stop = async () => {
+ try { process.kill(-child.pid, 'SIGTERM'); } catch { try { child.kill('SIGTERM'); } catch { /* it is gone */ } }
+ };
+ const ready = await waitForSite(url, { fetchImpl, timeoutMs });
+ if (!ready.ok) {
+ await stop();
+ const err = new Error('the repo\'s site did not start on ' + url + ' (' + ready.why + ')');
+ err.code = 'site.not_ready';
+ throw err;
+ }
+ return { url, stop };
+}
+
+/**
+ * The screenshots: the pages the change touches, at a phone's and a desktop's width.
+ *
+ * A page that is a file is shot from the working tree (a file:// URL), so a static site needs nothing running. A page
+ * that only exists once the app renders it (server/http/run.js, a template) has no file to open: the repo's own start
+ * script is brought up on a free port and the routes are shot there. If the site cannot start, the step says so
+ * instead of pretending a page was looked at.
+ */
+function screenshotAdapter({ run, playwright, chromePath, outDir, env = process.env, fetchImpl = globalThis.fetch, startSiteFn = startSite }) {
+ return async function screenshot({ files = [], routes = [], width = 390, cwd, pkg = null } = {}) {
+ if (!playwright) return { ok: true, skipped: true, summary: 'this machine has no browser to shoot the pages with' };
+ const where = cwd || run.cwd;
+ const all = Array.isArray(files) ? files : [];
+ const staticPages = all.filter((f) => /\.html?$/i.test(String(f)));
+ const rendered = all.filter((f) => !/\.html?$/i.test(String(f)));
+ const canServe = !!(env.CODES_PROOF_URL || (pkg && pkg.scripts && typeof pkg.scripts.start === 'string'));
+ const wantSite = canServe && rendered.length > 0;
+ if (!staticPages.length && !wantSite) {
+ return { ok: true, skipped: true, summary: 'no HTML page changed, and the repo has no site to render one' };
+ }
+ fs.mkdirSync(outDir, { recursive: true });
+ const shots = [];
+ for (const file of staticPages.slice(0, 5)) {
+ const abs = path.resolve(where, file);
+ if (!fs.existsSync(abs)) continue;
+ try {
+ shots.push(await shootPage({ playwright, chromePath, url: 'file://' + abs, width, outDir, route: file }));
+ } catch (e) {
+ shots.push({ ok: false, width, route: file, summary: 'the page could not be shot: ' + e.message });
+ }
+ }
+ if (wantSite) {
+ let site = null;
+ try {
+ site = await startSiteFn({ cwd: where, env, fetchImpl });
+ for (const route of (routes.length ? routes : ['/']).slice(0, 5)) {
+ const url = site.url.replace(/\/+$/, '') + (route.startsWith('/') ? route : '/' + route);
+ try {
+ shots.push({ live: true, ...(await shootPage({ playwright, chromePath, url, width, outDir, route })) });
+ } catch (e) {
+ shots.push({ ok: false, live: true, width, route, summary: 'the page could not be shot: ' + e.message });
+ }
+ }
+ } catch (e) {
+ // The change touches a page this repo renders; the site did not come up, so nobody looked at it.
+ return { ok: true, skipped: true, summary: 'the site did not start, so the pages were not shot: ' + e.message };
+ } finally {
+ if (site) await site.stop();
+ }
+ }
+ if (!shots.length) return { ok: true, skipped: true, summary: 'no page was there to shoot' };
+ const bad = shots.filter((s) => s.ok === false);
+ return {
+ ok: !bad.length, skipped: false,
+ path: shots[0].path, shots,
+ summary: bad.length ? bad.map((s) => s.width + 'px ' + s.route + ': ' + s.summary).join('; ')
+ : shots.length + ' page' + (shots.length === 1 ? '' : 's') + ' at ' + width + 'px, clean',
+ };
+ };
+}
+
+/** The OpenAI-compatible root a run token should be spent at: the job's own endpoint names, then the machine's. */
+function aiRoot(env = {}) {
+ if (env.OPENVIBE_AI_URL) return env.OPENVIBE_AI_URL;
+ if (env.OPENAI_BASE_URL) return env.OPENAI_BASE_URL;
+ if (env.DEEPSEEK_BASE_URL) return env.DEEPSEEK_BASE_URL;
+ if (env.ANTHROPIC_BASE_URL) return String(env.ANTHROPIC_BASE_URL).replace(/\/+$/, '') + '/v1';
+ return '';
+}
+
+/**
+ * The second opinion: OpenVibe.AI, with the task's run token.
+ *
+ * The token names the person and comes with the attempt (stage C3 mints it for the harness and puts it in the job's
+ * input). It is read from the job's own input, never from every provider key in the machine's environment: a personal
+ * OPENAI_API_KEY is not Codes' to spend at OpenVibe.AI. Without a token there is no second opinion, and the adapter
+ * says so rather than pretending one happened.
+ */
+function reviewAdapter({ env = {}, jobEnv = {}, fetchImpl = globalThis.fetch, baseUrl, models = [] } = {}) {
+ return async function review({ patch = '', summary = '', tests = null, model = null, family = null } = {}) {
+ // The token the attempt's own harness carries: any of the names C3 gives, that harness's provider first.
+ const runToken = jobEnv.OPENVIBE_AI_TOKEN || jobEnv.OPENAI_API_KEY || jobEnv.ANTHROPIC_AUTH_TOKEN || jobEnv.DEEPSEEK_API_KEY || '';
+ const token = runToken || env.OPENVIBE_CODES_TASK_TOKEN || env.OPENVIBE_AI_TOKEN || '';
+ if (!token) {
+ const err = new Error('no task token is connected yet, so no model could review this change');
+ err.code = 'review.no_token';
+ throw err;
+ }
+ const chosen = model || (models.find((m) => familyOf(m) && familyOf(m) !== family) || null);
+ if (!chosen) {
+ const err = new Error('no model of another family is connected, so nobody could review this change');
+ err.code = 'review.no_model';
+ throw err;
+ }
+ return await reviewWithAi({
+ baseUrl: baseUrl || aiRoot(jobEnv) || env.OPENVIBE_AI_URL || 'https://ai.openvibe.services/v1',
+ token, model: chosen, family: familyOf(chosen), patch, summary, tests, fetchImpl,
+ });
+ };
+}
+
+/**
+ * The adapters, for one repo.
+ *
+ * @param {object} what
+ * @param {string} what.cwd the repo the change is in
+ * @param {object} [what.env] the environment
+ * @param {function} [what.fetchImpl] fetch, for a test
+ * @param {string} [what.chromePath] the Chrome to shoot the pages with
+ * @param {string} [what.outDir] where the screenshots are written
+ * @returns {object} the adapters
+ */
+function createProofAdapters({ cwd, env = process.env, jobEnv = {}, fetchImpl = globalThis.fetch, chromePath = null, outDir = null, playwright = null, models = null, startSiteFn = startSite } = {}) {
+ const run = commandRunner({ env });
+ run.cwd = cwd;
+ const pw = playwright || findPlaywright(env);
+ const chrome = chromePath || env.CODES_CHROME_PATH || '/usr/bin/google-chrome';
+ const out = outDir || path.join(cwd, '.openvibe-proof');
+ // The models a reviewer may be chosen from: the catalog of the machine, or what the owner names.
+ const named = Array.isArray(models) && models.length ? models : String(env.CODES_PROOF_REVIEW_MODELS || '').split(',').map((s) => s.trim()).filter(Boolean);
+ return {
+ run,
+ diff: diffAdapter(run),
+ screenshot: screenshotAdapter({ run, playwright: pw, chromePath: chrome, outDir: out, env, fetchImpl, startSiteFn }),
+ review: reviewAdapter({ env, jobEnv, fetchImpl, models: named }),
+ models: named,
+ env,
+ };
+}
+
+module.exports = {
+ createProofAdapters, commandRunner, diffAdapter, screenshotAdapter, reviewAdapter,
+ findPlaywright, shootPage, freePort, waitForSite, startSite,
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+-- phase: expand
+-- Stage C8 (openvibe.codes v1): the proof of a finished task. "Proof, not promises": every task ends with its tests,
+-- its screenshots, its diff and a second model's review.
+--
+-- The items live in codes_task_proof, one row per proof item of one attempt: the tests the change reached, the pages
+-- it touched (phone and desktop), the diff, and the review. The task's row keeps the outcome in plain words
+-- ("Done with proof", "Didn't finish, with what is left", "Stopped by you") in run_tasks.outcome / outcome_note, as
+-- codes.task@2 states it.
+--
+-- The number is 0012 so it follows the run tokens of stage C3 (0011) without colliding with them.
+
+CREATE TABLE IF NOT EXISTS codes_task_proof (
+ id text COLLATE "C" PRIMARY KEY, -- prf_<ULID>
+ task_id text COLLATE "C" NOT NULL, -- tsk_<ULID>
+ attempt_n integer NOT NULL DEFAULT 0, -- the attempt the proof belongs to (0: the task itself)
+ n integer NOT NULL, -- the item's place in the proof (1, 2, …)
+ kind text COLLATE "C" NOT NULL, -- tests | screenshots | diff | review
+ label text COLLATE "C" NOT NULL, -- what a person reads: 'tests', 'screenshots', …
+ ok boolean NOT NULL, -- did it pass
+ skipped boolean NOT NULL DEFAULT false, -- it could not run here (and why is in summary)
+ summary text COLLATE "C", -- one line, in plain words
+ family text COLLATE "C", -- the reviewer's model family
+ detail jsonb, -- the item's own data (shots, stats, the patch)
+ created_at timestamptz NOT NULL DEFAULT now()
+);
+-- The fence: one proof item of one kind per attempt, so a second ending of the same attempt (the portal's
+-- 10-30 bug) writes no second row and no second event.
+CREATE UNIQUE INDEX IF NOT EXISTS codes_task_proof_item ON codes_task_proof (task_id, attempt_n, kind);
+CREATE UNIQUE INDEX IF NOT EXISTS codes_task_proof_task_n ON codes_task_proof (task_id, n);
+CREATE INDEX IF NOT EXISTS codes_task_proof_task ON codes_task_proof (task_id, attempt_n);
@@ -33,3 +33,11 @@
@@ -33,3 +33,11 @@
.run-patch .hunk { color: var(--accent-light, #60a5fa); }
.run-patch .hunk { color: var(--accent-light, #60a5fa); }
.run-repo { overflow-wrap: break-word; }
.run-repo { overflow-wrap: break-word; }
.run-host { display: block; font-size: 0.55em; font-weight: 500; color: var(--text-muted, #8b9bb4); }
.run-host { display: block; font-size: 0.55em; font-weight: 500; color: var(--text-muted, #8b9bb4); }
+
+/* The proof of a finished task (stage C8): the tests, the pages, the diff and the review. */
+.run-proof { margin: 16px 0; max-width: 820px; }
+.run-proof-items { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
+.run-proof-item { border: 1px solid var(--border, #1f2d47); border-radius: 12px; padding: 10px 14px; background: var(--bg-card, #131c2e); }
+.run-proof-head { margin: 0; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
+.run-proof-summary { margin: 4px 0 0; color: var(--text-muted, #7386a3); overflow-wrap: anywhere; }
+.run-proof-item[data-ok="false"] { border-color: var(--danger, #e5484d); }
new file mode 100644
new file mode 100644
@@ -0,0 +1,144 @@
@@ -0,0 +1,144 @@
+'use strict';
+
+/**
+ * A finished task's proof: its tests, its pages, its diff and the second model's opinion.
+ *
+ * The proof arrives with the machine's result (the job worker collected it there, in the clone the agent worked in),
+ * and this module keeps it: one row per proof item, one event per item on the task's stream, and the task's outcome
+ * in the plain words a person reads.
+ *
+ * createProof({ store, config, log })
+ * record(taskId, attemptN, items) the items of one attempt: the rows, and the lines for the stream
+ * list(taskId) the items of a task, in order
+ * present(items) the items as the API shows them
+ * outcomeOf(items, fallback) the outcome in plain words
+ *
+ * The outcome is what the spec asks for: "Done with proof", "Didn't finish, with what is left" and "Stopped by
+ * you". The engine's own verdict stands (a task the agent did not finish is unfinished); the proof says out loud
+ * what the evidence says, so a task that claims to be done and whose tests fail does not pass for done.
+ */
+
+const { proofNote } = require('../../engine/proof');
+
+/** The kinds of proof item, in the order they are read. */
+const KINDS = ['tests', 'screenshots', 'diff', 'review'];
+
+/** How much of an item's own data is kept. */
+const MAX_DETAIL = 64 * 1024;
+
+const iso = (v) => {
+ if (!v) return null;
+ const t = v instanceof Date ? v.getTime() : Date.parse(String(v));
+ return Number.isFinite(t) ? new Date(t).toISOString() : null;
+};
+
+const clip = (s, n) => String(s == null ? '' : s).slice(0, n);
+
+function createProof({ store, config = {}, log = console }) {
+ const db = store.db;
+
+ /**
+ * The proof of one attempt: the rows, and the lines the task's stream shows (one per item).
+ *
+ * Recording the same attempt twice changes nothing: the row's place in the proof is fixed (the attempt's number
+ * and the item's), so a second recording finds the rows there and writes no second event.
+ *
+ * @param {string} taskId the task
+ * @param {number|null} attemptN the attempt the proof belongs to
+ * @param {object[]} items the proof items
+ * @returns {Promise<{ rows: object[], lines: object[] }>} the rows, and the lines to append to the stream
+ */
+ async function record(taskId, attemptN, items) {
+ const list = Array.isArray(items) ? items : [];
+ if (!taskId || !list.length) return { rows: [], lines: [] };
+ const known = await db.many('SELECT id, n FROM codes_task_proof WHERE task_id = $1', [taskId]);
+ const seen = new Set(known.map((r) => Number(r.n)));
+ const attempt = attemptN == null ? 0 : Number(attemptN);
+ const stored = [];
+ for (const item of list) {
+ const kind = KINDS.includes(item && item.kind) ? item.kind : 'note';
+ let n = stored.length + 1;
+ while (seen.has(n)) n++;
+ seen.add(n);
+ const id = store.newId('prf');
+ const detail = item && item.detail !== undefined ? item.detail : item;
+ let body = null;
+ try { body = JSON.stringify(detail); } catch { body = null; }
+ if (body && body.length > MAX_DETAIL) body = body.slice(0, MAX_DETAIL);
+ const row = await db.maybe(
+ 'INSERT INTO codes_task_proof (id, task_id, attempt_n, n, kind, label, ok, skipped, summary, family, detail, created_at)'
+ + ' VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12::timestamptz)'
+ + ' ON CONFLICT DO NOTHING RETURNING *',
+ [id, taskId, attempt, n, kind, clip(item && item.label, 80),
+ item && item.ok === false ? false : true, item && item.skipped === true ? true : false,
+ clip(item && item.summary, 1000), clip(item && item.family, 80), body, store.iso()],
+ );
+ if (row) stored.push(row);
+ }
+ // One line per item on the task's stream, after the lines that are already there.
+ let seq = await nextSeq(taskId);
+ const at = store.iso();
+ const lines = stored.map((row) => ({
+ seq: seq++, at, kind: 'proof',
+ body: {
+ t: 'proof', kind: row.kind, label: row.label, ok: row.ok === true,
+ skipped: row.skipped === true, summary: row.summary || null, family: row.family || null,
+ },
+ }));
+ return { rows: stored, lines };
+ }
+
+ /** The first seq on the task's stream that nothing uses. */
+ async function nextSeq(taskId) {
+ const rows = await db.many('SELECT seq FROM codes_session_lines WHERE task_id = $1', [taskId]);
+ const taken = new Set(rows.map((r) => Number(r.seq)));
+ const max = rows.reduce((m, r) => Math.max(m, Number(r.seq) || 0), 0);
+ let seq = max + 1;
+ let tries = 0;
+ while (taken.has(seq) && tries++ < 10000) seq++;
+ return seq;
+ }
+
+ /** The items of a task, in order. */
+ async function list(taskId) {
+ if (!taskId) return [];
+ return await db.many('SELECT * FROM codes_task_proof WHERE task_id = $1 ORDER BY n ASC', [taskId]);
+ }
+
+ /** The items as the API shows them. */
+ function present(items) {
+ return (Array.isArray(items) ? items : []).map((r) => ({
+ kind: r.kind,
+ label: r.label,
+ ok: r.ok === true,
+ skipped: r.skipped === true,
+ summary: r.summary || null,
+ family: r.family || null,
+ attempt_n: r.attempt_n == null ? null : Number(r.attempt_n),
+ detail: r.detail || null,
+ created_at: iso(r.created_at),
+ }));
+ }
+
+ /**
+ * The outcome in plain words, from the proof.
+ *
+ * @param {object[]} items the proof items
+ * @param {object} [fallback] { state, note } the outcome without a proof (the engine's own verdict)
+ * @returns {{ state: string, note: string }} the outcome
+ */
+ function outcomeOf(items, fallback = {}) {
+ const list = Array.isArray(items) ? items : [];
+ if (!list.length) return { state: fallback.state || null, note: fallback.note || null };
+ const failed = list.filter((i) => i && i.ok === false);
+ const note = proofNote(list);
+ // Proof, not promises: when the evidence failed, the task is not called done, whatever the agent claimed. A task
+ // the person stopped stays stopped: that outcome is theirs, not the evidence's.
+ const state = fallback.state === 'stopped' ? 'stopped' : (failed.length ? 'unfinished' : (fallback.state || 'done'));
+ return { state, note: note + (fallback.note ? ' ' + fallback.note : '') };
+ }
+
+ return { KINDS, record, list, present, outcomeOf };
+}
+
+module.exports = { createProof, KINDS };
@@ -140,12 +140,14 @@ function usageOf(result) {
@@ -140,12 +140,14 @@ function usageOf(result) {
};
};
}
}
-function createRunTasks({ store, run, config, log = console, sessions = null, outbox = null, runTokens = null, connections = null, attempts = null }) {
+function createRunTasks({ store, run, config, log = console, sessions = null, outbox = null, runTokens = null, connections = null, attempts = null, proof = null }) {
const db = store.db;
const db = store.db;
const who = (row) => ({ subject: row.subject, project: row.project_id });
const who = (row) => ({ subject: row.subject, project: row.project_id });
// Lazily, so a caller that only reads tasks does not need the session/outbox modules (and there is no cycle).
// Lazily, so a caller that only reads tasks does not need the session/outbox modules (and there is no cycle).
const sessionLines = sessions || require('./session-lines').createSessionLines({ store, config, log });
const sessionLines = sessions || require('./session-lines').createSessionLines({ store, config, log });
const taskOutbox = outbox || require('../events/outbox').createOutbox({ store, log });
const taskOutbox = outbox || require('../events/outbox').createOutbox({ store, log });
+ // The proof of a finished task (stage C8): the tests, the pages, the diff and the second model's opinion.
+ const taskProof = proof || require('./proof').createProof({ store, config, log });
const attemptStore = attempts || require('./attempts').createAttempts({ store, config, log });
const attemptStore = attempts || require('./attempts').createAttempts({ store, config, log });
/** An attempt's state when it ended: a limit reached is a budget-stop, never a plain failure. */
/** An attempt's state when it ended: a limit reached is a budget-stop, never a plain failure. */
const ENDED = new Set(['done', 'failed', 'stopped', 'budget-stopped', 'interrupted']);
const ENDED = new Set(['done', 'failed', 'stopped', 'budget-stopped', 'interrupted']);
@@ -389,16 +391,40 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
@@ -389,16 +391,40 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
const attempts = await attemptStore.list(row.id);
const attempts = await attemptStore.list(row.id);
const ending = attempts[attempts.length - 1];
const ending = attempts[attempts.length - 1];
if (!ending || !ENDED.has(ending.state)) return row;
if (!ending || !ENDED.has(ending.state)) return row;
+ // The proof this attempt's machine collected: the items are kept, and each one is an event on the stream.
+ const collected = await recordProof(row, ending);
const plan = await attemptStore.decide({ task: row, attempts, ending, result: row.result });
const plan = await attemptStore.decide({ task: row, attempts, ending, result: row.result });
if (plan.outcome) {
if (plan.outcome) {
+ // The outcome in plain words: what the engine decided, and what the proof says about it.
+ const outcome = taskProof.outcomeOf(collected, { state: plan.outcome, note: plan.note });
return await db.one('UPDATE run_tasks SET outcome = $2, outcome_note = $3, updated_at = $4 WHERE id = $1 RETURNING *',
return await db.one('UPDATE run_tasks SET outcome = $2, outcome_note = $3, updated_at = $4 WHERE id = $1 RETURNING *',
- [row.id, plan.outcome, plan.note, store.now()]);
+ [row.id, outcome.state, outcome.note, store.now()]);
}
}
if (!plan.kind) return row;
if (!plan.kind) return row;
const done = await startFollowUp(row, plan, ending);
const done = await startFollowUp(row, plan, ending);
return done.row || row;
return done.row || row;
}
}
+ /**
+ * The proof this attempt's machine collected: the items are kept, and each one is an event on the task's stream.
+ *
+ * The machine collects it in the clone it worked in (harness/job.js), so the tests that ran and the pages that were
+ * shot are about that machine's work. A task from before this change has no proof, and says nothing about one.
+ *
+ * @param {object} row the task
+ * @param {object} ending the attempt that ended
+ * @returns {Promise<object[]>} the items as they are stored
+ */
+ async function recordProof(row, ending) {
+ const result = asObject(row.result) || {};
+ const items = Array.isArray(result.proof && result.proof.items) ? result.proof.items : [];
+ if (!items.length) return [];
+ const attemptN = ending ? Number(ending.n) : null;
+ const recorded = await taskProof.record(row.id, attemptN, items);
+ if (recorded.lines.length) await sessionLines.append(row.id, recorded.lines, attemptN);
+ return recorded.rows;
+ }
+
/**
/**
* The next attempt: a Run job for the task with the engine's plan in it. The attempt is written first and the job
* The next attempt: a Run job for the task with the engine's plan in it. The attempt is written first and the job
* second, so the fence (follow_up_of) is in place before anything is started.
* second, so the fence (follow_up_of) is in place before anything is started.
@@ -542,7 +568,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
@@ -542,7 +568,7 @@ function createRunTasks({ store, run, config, log = console, sessions = null, ou
}
}
return {
return {
- start, refresh, applyJob, applyEvent, recordState, afterEnd, continueTask, stopTask,
+ start, refresh, applyJob, applyEvent, recordState, afterEnd, continueTask, stopTask, proof: taskProof,
latestProject, present, who, attempts: attemptStore,
latestProject, present, who, attempts: attemptStore,
};
};
}
}
@@ -190,8 +190,13 @@ ${typeof d.duration_ms === 'number' ? html`<p class="muted">${Math.round(d.durat
@@ -190,8 +190,13 @@ ${typeof d.duration_ms === 'number' ? html`<p class="muted">${Math.round(d.durat
async function timelineHtml(row) {
async function timelineHtml(row) {
const attempts = await taskStore.attempts.list(row.id);
const attempts = await taskStore.attempts.list(row.id);
if (!attempts.length) return '';
if (!attempts.length) return '';
+ // The outcome in plain words: the label, and the note beside it — unless the note already says it
+ // ("Done with proof: …" says "Done" already, so it is not said twice).
+ const note = row.outcome_note || '';
+ const label = row.outcome ? (OUTCOME_TEXT[row.outcome] || row.outcome) : '';
+ const says = !!label && note.toLowerCase().startsWith(label.toLowerCase());
const outcome = row.outcome
const outcome = row.outcome
- ? html`<p class="run-outcome" data-outcome="${row.outcome}"><b>${OUTCOME_TEXT[row.outcome] || row.outcome}</b>${row.outcome_note ? html` ${row.outcome_note}` : ''}</p>`
+ ? html`<p class="run-outcome" data-outcome="${row.outcome}"><b>${says ? note : [label, note].filter(Boolean).join(' ')}</b></p>`
: '';
: '';
return html`<section class="run-timeline" data-timeline data-attempts="${attempts.length}">
return html`<section class="run-timeline" data-timeline data-attempts="${attempts.length}">
<h2>The attempts</h2>
<h2>The attempts</h2>
@@ -204,6 +209,23 @@ ${outcome}
@@ -204,6 +209,23 @@ ${outcome}
</section>`;
</section>`;
}
}
+ /**
+ * The task's proof (stage C8): the tests, the pages, the diff and the second model's opinion, each with what it
+ * found. The outcome in plain words sits above it, so a person reads what was proved before the detail.
+ */
+ async function proofHtml(row) {
+ const items = await taskStore.proof.list(row.id);
+ if (!items.length) return '';
+ const shown = taskStore.proof.present(items);
+ return html`<section class="run-proof" data-proof data-items="${shown.length}">
+<h2>The proof</h2>
+<ul class="run-proof-items">${shown.map((i) => html`<li class="run-proof-item" data-proof-kind="${i.kind}" data-ok="${i.ok}" data-skipped="${i.skipped}">
+<p class="run-proof-head"><b>${i.label}</b>${i.skipped ? html` <span class="run-pill">skipped</span>` : (i.ok ? html` <span class="run-pill run-pill-succeeded">passed</span>` : html` <span class="run-pill run-pill-failed">failed</span>`)}${i.family ? html` · ${i.family}` : ''}</p>
+${i.summary ? html`<p class="run-proof-summary" data-proof-summary>${i.summary}</p>` : ''}
+</li>`)}</ul>
+</section>`;
+ }
+
r.get('/run/tasks/:id', async (req, res) => {
r.get('/run/tasks/:id', async (req, res) => {
if (!signedIn(req)) return res.redirect(302, `/auth/login?next=${encodeURIComponent(req.originalUrl)}`);
if (!signedIn(req)) return res.redirect(302, `/auth/login?next=${encodeURIComponent(req.originalUrl)}`);
let row = await own(req, req.params.id);
let row = await own(req, req.params.id);
@@ -220,6 +242,7 @@ ${outcome}
@@ -220,6 +242,7 @@ ${outcome}
<p><span class="run-pill run-pill-${row.state}" data-state>${STATE_LABEL[row.state] || row.state}</span>${row.agent ? html` <span class="run-pill">${row.agent}</span>` : ''}</p>
<p><span class="run-pill run-pill-${row.state}" data-state>${STATE_LABEL[row.state] || row.state}</span>${row.agent ? html` <span class="run-pill">${row.agent}</span>` : ''}</p>
<pre tabindex="0" class="run-prompt">${row.prompt}</pre>
<pre tabindex="0" class="run-prompt">${row.prompt}</pre>
${await timelineHtml(row)}
${await timelineHtml(row)}
+${await proofHtml(row)}
${live ? html`<section class="run-live" data-events="/run/tasks/${row.id}/events" data-title="${title}" aria-live="polite"><h2>Live</h2><ol class="run-log" data-log></ol></section>
${live ? html`<section class="run-live" data-events="/run/tasks/${row.id}/events" data-title="${title}" aria-live="polite"><h2>Live</h2><ol class="run-log" data-log></ol></section>
<form method="post" action="/run/tasks/${row.id}/cancel"><button class="run-btn" type="submit">Cancel</button></form>` : html`${session}${resultHtml(row, { session: !!(lines && lines.length) })}`}`;
<form method="post" action="/run/tasks/${row.id}/cancel"><button class="run-btn" type="submit">Cancel</button></form>` : html`${session}${resultHtml(row, { session: !!(lines && lines.length) })}`}`;
return page(req, res, { title: 'Task', body, sharedScripts: ['agent-session.js'] });
return page(req, res, { title: 'Task', body, sharedScripts: ['agent-session.js'] });
@@ -96,11 +96,38 @@ function createTaskEvents({ store, config = {}, log = console, attempts = null }
@@ -96,11 +96,38 @@ function createTaskEvents({ store, config = {}, log = console, attempts = null }
};
};
await sendLines();
await sendLines();
+ // The proof of the task (stage C8): one event per item — the tests, the pages, the diff, the review.
+ const sentProof = new Set();
+ const sendProof = async () => {
+ const items = await db.many('SELECT * FROM codes_task_proof WHERE task_id = $1 ORDER BY n ASC', [row.id]);
+ let wrote = 0;
+ for (const item of items) {
+ if (sentProof.has(item.id)) continue;
+ sentProof.add(item.id);
+ wrote++;
+ write(frame('proof', {
+ id: item.id,
+ n: Number(item.n),
+ kind: item.kind,
+ label: item.label,
+ ok: item.ok === true,
+ skipped: item.skipped === true,
+ summary: item.summary || null,
+ family: item.family || null,
+ attempt_n: item.attempt_n == null ? null : Number(item.attempt_n),
+ at: item.created_at ? new Date(item.created_at).toISOString() : null,
+ }));
+ }
+ return wrote;
+ };
+ await sendProof();
+
const tick = async () => {
const tick = async () => {
if (closed || busy) return;
if (closed || busy) return;
busy = true;
busy = true;
try {
try {
const sentLines = await sendLines();
const sentLines = await sendLines();
+ await sendProof();
const fresh = await db.maybe('SELECT * FROM run_tasks WHERE id = $1', [row.id]);
const fresh = await db.maybe('SELECT * FROM run_tasks WHERE id = $1', [row.id]);
const rows = await attemptStore.list(row.id);
const rows = await attemptStore.list(row.id);
let updates = '';
let updates = '';
@@ -11,7 +11,7 @@ const DIR = path.join(__dirname, '..', 'engine');
@@ -11,7 +11,7 @@ const DIR = path.join(__dirname, '..', 'engine');
(async () => {
(async () => {
await check('the engine exports every module and the names a caller wants', () => {
await check('the engine exports every module and the names a caller wants', () => {
- for (const name of ['router', 'chain', 'budget', 'transcript', 'effort', 'scope', 'runerror']) {
+ for (const name of ['router', 'chain', 'budget', 'transcript', 'effort', 'scope', 'runerror', 'proof', 'review']) {
assert.ok(engine[name], `${name} is exported`);
assert.ok(engine[name], `${name} is exported`);
}
}
for (const name of ['classify', 'pick', 'outcome', 'nextStep', 'evaluate', 'limitsFor', 'parse', 'parseText', 'fitEffort', 'pickModel']) {
for (const name of ['classify', 'pick', 'outcome', 'nextStep', 'evaluate', 'limitsFor', 'parse', 'parseText', 'fitEffort', 'pickModel']) {
new file mode 100644
new file mode 100644
@@ -0,0 +1,188 @@
@@ -0,0 +1,188 @@
+'use strict';
+
+/**
+ * Stage C8, the engine half: the proof collector (engine/proof.js) and the second opinion (engine/review.js).
+ *
+ * The collector asks for four things — the repo's own tests, the pages the diff touches, the diff, and a model of
+ * another family — and every one of them is an adapter, so a fake stands in for the machine here: the tests never run,
+ * no browser opens and no model is called.
+ */
+const assert = require('assert');
+const { collectProof, testPlan, testWords, pageFiles, routesOf, outcomeOf } = require('../engine/proof');
+const { check, done } = require('./helpers/boot');
+
+(async () => {
+ /** A fake machine: the repo's package.json, a passing test run, a clean page and a second model's opinion. */
+ const machine = ({ pkg = { scripts: { test: 'node test/run.js' } }, files = [], testExit = 0, review = null, shots = [] } = {}) => {
+ const calls = [];
+ const run = async (argv, options) => {
+ calls.push(argv);
+ if (argv[0] === 'node' && argv[1] === '-e') return { code: 0, output: JSON.stringify(pkg) };
+ if (argv[0] === 'node' && argv[1] === 'test/run.js') return { code: testExit, output: testExit === 0 ? '12/12 test files passed' : 'FAIL test/foo.test.js\nAssertionError' };
+ if (argv[0] === 'npm') return { code: testExit, output: testExit === 0 ? '12/12 test files passed' : 'FAIL test/foo.test.js\nAssertionError' };
+ return { code: 0, output: '' };
+ };
+ return {
+ calls,
+ adapters: {
+ run,
+ diff: async () => ({ files, stats: files.length + ' files changed', patch: '+ the change', summary: files.length + ' files changed' }),
+ screenshot: async ({ width }) => (shots.find((s) => s.width === width) || { ok: true, path: `/proof/${width}.png`, summary: 'the page renders clean' }),
+ review: review || (async () => ({ family: 'deepseek', verdict: 'approved', summary: 'It holds together.' })),
+ models: ['claude-sonnet-4', 'deepseek-chat', 'gpt-5'],
+ env: {},
+ },
+ };
+ };
+
+ try {
+ await check('the collector asks for the four things, in order, and says what each one found', async () => {
+ const m = machine({ files: ['engine/proof.js', 'public/index.html'] });
+ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters, author: { agent: 'claude-code', model: 'claude-sonnet-4' } });
+ assert.deepStrictEqual(proof.items.map((i) => i.kind), ['tests', 'screenshots', 'diff', 'review']);
+ assert.strictEqual(proof.outcome.state, 'done');
+ assert.ok(/^Done with proof/.test(proof.outcome.note), proof.outcome.note);
+ assert.strictEqual(proof.review.family, 'deepseek');
+ });
+
+ await check('the repo\'s own runner is used, narrowed to the tests the change reaches', async () => {
+ const m = machine({ files: ['engine/proof.js'] });
+ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });
+ const test = proof.items.find((i) => i.kind === 'tests');
+ assert.strictEqual(test.ok, true);
+ assert.strictEqual(test.command, 'node test/run.js proof');
+ assert.strictEqual(test.mode, 'targeted');
+ });
+
+ await check('a test script with its own flags is left to npm, and the words follow --', async () => {
+ const plan = testPlan({ pkg: { scripts: { test: 'node --test test/' } }, files: ['engine/proof.js'], env: {} });
+ assert.strictEqual(plan.command, 'npm');
+ assert.deepStrictEqual(plan.args, ['test', '--', 'proof']);
+ });
+
+ await check('the whole suite runs only when the change touches what every test shares and the guard allows it', async () => {
+ const shared = testPlan({ pkg: { scripts: { test: 'node test/run.js' } }, files: ['package.json'], env: {} });
+ assert.strictEqual(shared.mode, 'targeted');
+ assert.ok(/full suite is not allowed/.test(shared.reason), shared.reason);
+ const allowed = testPlan({ pkg: { scripts: { test: 'node test/run.js' } }, files: ['package.json'], env: { CODES_PROOF_FULL_SUITE: '1' } });
+ assert.strictEqual(allowed.mode, 'full');
+ assert.deepStrictEqual(allowed.args, ['test/run.js']);
+ });
+
+ await check('a repo with no test command of its own says so instead of failing', async () => {
+ const m = machine({ pkg: { scripts: {} } });
+ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });
+ const test = proof.items.find((i) => i.kind === 'tests');
+ assert.strictEqual(test.skipped, true);
+ assert.ok(/no test command/.test(test.summary), test.summary);
+ });
+
+ await check('the pages the diff touches are shot at a phone\'s and a desktop\'s width', async () => {
+ const m = machine({ files: ['public/index.html', 'server/http/run.js', 'engine/proof.js'] });
+ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });
+ const shots = proof.items.find((i) => i.kind === 'screenshots');
+ assert.strictEqual(shots.ok, true);
+ assert.deepStrictEqual(shots.shots.map((s) => s.width), [390, 1440]);
+ });
+
+ await check('a change that touches no page says so, and does not open a browser', async () => {
+ const m = machine({ files: ['engine/proof.js'] });
+ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });
+ const shots = proof.items.find((i) => i.kind === 'screenshots');
+ assert.strictEqual(shots.skipped, true);
+ assert.ok(/touches no page/.test(shots.summary), shots.summary);
+ });
+
+ await check('a machine with nothing to shoot with says the screenshots were skipped, and why', async () => {
+ const m = machine({ files: ['public/index.html'] });
+ m.adapters.screenshot = async ({ width }) => ({ ok: true, skipped: true, width, summary: 'the site did not start, so the pages were not shot' });
+ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });
+ const shots = proof.items.find((i) => i.kind === 'screenshots');
+ assert.strictEqual(shots.skipped, true);
+ assert.ok(/did not start/.test(shots.summary), shots.summary);
+ assert.ok(/screenshots was skipped/.test(proof.outcome.note), proof.outcome.note);
+ });
+
+ await check('a page that scrolls sideways is a failure of the proof, not a picture of one', async () => {
+ const m = machine({ files: ['public/index.html'], shots: [{ width: 390, ok: false, summary: 'the page is 480px wide, so it scrolls sideways on a 390px screen' }] });
+ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });
+ const shots = proof.items.find((i) => i.kind === 'screenshots');
+ assert.strictEqual(shots.ok, false);
+ assert.ok(/scrolls sideways/.test(shots.summary), shots.summary);
+ assert.strictEqual(proof.outcome.state, 'unfinished');
+ assert.ok(/Didn't finish, with what is left/.test(proof.outcome.note), proof.outcome.note);
+ });
+
+ await check('a review by the author\'s own family is refused: it is not a second opinion', async () => {
+ const m = machine({ files: ['engine/proof.js'], review: async () => ({ family: 'anthropic', verdict: 'approved', summary: 'Fine.' }) });
+ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters, author: { agent: 'claude-code', model: 'claude-sonnet-4' } });
+ const review = proof.items.find((i) => i.kind === 'review');
+ assert.strictEqual(review.ok, false);
+ assert.ok(/own family/.test(review.summary), review.summary);
+ assert.strictEqual(proof.review, null);
+ });
+
+ await check('a change request from the second model is said out loud, not hidden', async () => {
+ const m = machine({ files: ['engine/proof.js'], review: async () => ({ family: 'deepseek', verdict: 'changes_requested', summary: 'The parser still throws on an empty file.' }) });
+ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters, author: { agent: 'claude-code', model: 'claude-sonnet-4' } });
+ const review = proof.items.find((i) => i.kind === 'review');
+ assert.strictEqual(review.ok, false);
+ assert.ok(/empty file/.test(review.summary), review.summary);
+ assert.strictEqual(proof.outcome.state, 'unfinished');
+ });
+
+ await check('no reviewer connected is a skip, not a failure', async () => {
+ const m = machine({ files: ['engine/proof.js'] });
+ const proof = await collectProof({ cwd: '/repo', adapters: { ...m.adapters, review: undefined, models: [] } });
+ const review = proof.items.find((i) => i.kind === 'review');
+ assert.strictEqual(review.skipped, true);
+ assert.strictEqual(proof.outcome.state, 'done');
+ });
+
+ await check('a machine with only the author\'s family says nobody reviewed, and is not called unfinished', async () => {
+ const m = machine({ files: ['engine/proof.js'] });
+ const proof = await collectProof({
+ cwd: '/repo', adapters: { ...m.adapters, models: ['claude-sonnet-4', 'claude-opus-4'] },
+ author: { agent: 'claude-code', model: 'claude-sonnet-4' },
+ });
+ const review = proof.items.find((i) => i.kind === 'review');
+ assert.strictEqual(review.skipped, true);
+ assert.ok(/no model of another family/.test(review.summary), review.summary);
+ assert.strictEqual(proof.outcome.state, 'done');
+ assert.ok(/review was skipped/.test(proof.outcome.note), proof.outcome.note);
+ });
+
+ await check('the diff is collected: the files, the stats and the patch', async () => {
+ const m = machine({ files: ['engine/proof.js'] });
+ const proof = await collectProof({ cwd: '/repo', adapters: m.adapters });
+ const diff = proof.items.find((i) => i.kind === 'diff');
+ assert.deepStrictEqual(diff.files, ['engine/proof.js']);
+ assert.strictEqual(diff.patch, '+ the change');
+ });
+
+ await check('every item is emitted as it is collected, so the page can show it live', async () => {
+ const m = machine({ files: ['engine/proof.js'] });
+ const seen = [];
+ await collectProof({ cwd: '/repo', adapters: m.adapters, emit: async (item) => seen.push(item.kind) });
+ assert.deepStrictEqual(seen, ['tests', 'screenshots', 'diff', 'review']);
+ });
+
+ await check('the changed files that are pages, and the routes they are served at', async () => {
+ assert.deepStrictEqual(pageFiles(['public/index.html', 'engine/proof.js']), ['public/index.html']);
+ assert.deepStrictEqual(routesOf(['public/index.html', 'public/a/b.html']), ['/', '/a/b']);
+ assert.deepStrictEqual(testWords(['engine/proof.js']), ['proof']);
+ });
+
+ await check('the outcome in plain words: done with proof, or what is left', async () => {
+ assert.strictEqual(outcomeOf([{ kind: 'tests', label: 'tests', ok: true }]).state, 'done');
+ assert.ok(/Done with proof/.test(outcomeOf([{ kind: 'tests', label: 'tests', ok: true }]).note));
+ assert.strictEqual(outcomeOf([{ kind: 'tests', label: 'tests', ok: false, summary: 'it failed' }]).state, 'unfinished');
+ });
+
+ console.log('proof: the tests, the pages, the diff and the second opinion are collected, and said in plain words');
+ } catch (e) {
+ console.error(e);
+ process.exit(1);
+ }
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
+'use strict';
+
+/**
+ * Stage C8, the second opinion: engine/review.js with a fake fetch.
+ *
+ * The rule that makes the review worth reading is the family: a model of the family that wrote the change repeats its
+ * own reasoning, so its verdict is refused. The call itself is one request to OpenVibe.AI, read as a verdict even
+ * when the model answers in prose.
+ */
+const assert = require('assert');
+const {
+ familyOf, authorFamily, pickReviewer, reviewPrompt, readVerdict, reviewWithAi,
+} = require('../engine/review');
+const { check, done } = require('./helpers/boot');
+
+const OK = async () => ({ status: 200, ok: true, json: async () => ({ choices: [{ message: { content: '{"verdict":"approved","summary":"It holds together."}' } }] }) });
+
+(async () => {
+ try {
+ await check('the model families are read from the model\'s own name', () => {
+ assert.strictEqual(familyOf('claude-sonnet-4'), 'anthropic');
+ assert.strictEqual(familyOf('gpt-5'), 'openai');
+ assert.strictEqual(familyOf('deepseek-chat'), 'deepseek');
+ assert.strictEqual(familyOf('gemini-2.5-pro'), 'google');
+ assert.strictEqual(familyOf('something-else'), null);
+ assert.strictEqual(authorFamily({ agent: 'claude-code' }), 'anthropic');
+ assert.strictEqual(authorFamily({ agent: 'codex', model: 'gpt-5' }), 'openai');
+ });
+
+ await check('the reviewer is a model of another family', () => {
+ const picked = pickReviewer({ models: ['claude-sonnet-4', 'deepseek-chat'], author: 'anthropic' });
+ assert.strictEqual(picked.family, 'deepseek');
+ assert.strictEqual(pickReviewer({ models: ['claude-sonnet-4'], author: 'anthropic' }), null);
+ });
+
+ await check('a verdict is read from JSON or from the words, and an empty answer is a change request', () => {
+ assert.strictEqual(readVerdict('{"verdict":"approved","summary":"ok"}').verdict, 'approved');
+ assert.strictEqual(readVerdict('looks good to me').verdict, 'approved');
+ assert.strictEqual(readVerdict('this needs changes').verdict, 'changes_requested');
+ assert.strictEqual(readVerdict('').verdict, 'changes_requested');
+ assert.strictEqual(readVerdict('{"verdict":"changes_requested","summary":"no"}').verdict, 'changes_requested');
+ });
+
+ await check('the prompt carries the change, what it was for, and the tests, and asks for JSON', () => {
+ const prompt = reviewPrompt({ patch: '+const x = 1;', summary: 'add x', tests: '3 passed' });
+ assert.ok(prompt.includes('+const x = 1;'), 'the diff is in the prompt');
+ assert.ok(prompt.includes('add x'), 'what the change was for is in the prompt');
+ assert.ok(prompt.includes('3 passed'), 'the tests are in the prompt');
+ assert.ok(/approved/.test(prompt) && /changes_requested/.test(prompt), 'the verdict is asked for');
+ });
+
+ await check('the review is one chat completion, with the token, read as a verdict', async () => {
+ const calls = [];
+ const fetchImpl = async (url, options) => { calls.push({ url, options }); return OK(); };
+ const verdict = await reviewWithAi({
+ baseUrl: 'https://ai.openvibe.services/v1', token: 'test-token', model: 'deepseek-chat',
+ patch: '+x', summary: 'a change', fetchImpl,
+ });
+ assert.strictEqual(calls.length, 1);
+ assert.strictEqual(calls[0].url, 'https://ai.openvibe.services/v1/chat/completions');
+ assert.strictEqual(calls[0].options.headers.authorization, 'Bearer test-token');
+ assert.strictEqual(JSON.parse(calls[0].options.body).model, 'deepseek-chat');
+ assert.strictEqual(verdict.verdict, 'approved');
+ assert.strictEqual(verdict.family, 'deepseek');
+ });
+
+ await check('a provider that speaks the messages dialect is tried when chat completions is not there', async () => {
+ const calls = [];
+ const fetchImpl = async (url) => {
+ calls.push(url);
+ if (url.endsWith('/chat/completions')) return { status: 404, ok: false, json: async () => ({}) };
+ return { status: 200, ok: true, json: async () => ({ content: [{ type: 'text', text: 'changes requested: the parser still throws' }] }) };
+ };
+ const verdict = await reviewWithAi({ token: 'test-token', model: 'claude-sonnet-4', patch: '+x', fetchImpl });
+ assert.ok(calls[1].endsWith('/messages'), calls[1]);
+ assert.strictEqual(verdict.verdict, 'changes_requested');
+ });
+
+ await check('a review needs the token and the model, and says which is missing', async () => {
+ await assert.rejects(() => reviewWithAi({ model: 'x', fetchImpl: OK }), /token/);
+ await assert.rejects(() => reviewWithAi({ token: 't', fetchImpl: OK }), /model/);
+ });
+
+ await check('a refused review is an error, not a verdict', async () => {
+ const fetchImpl = async () => ({ status: 429, ok: false, json: async () => ({}) });
+ await assert.rejects(() => reviewWithAi({ token: 't', model: 'deepseek-chat', patch: '+x', fetchImpl }), /refused the review \(HTTP 429\)/);
+ });
+
+ console.log('review: a model of another family, one call to OpenVibe.AI, and a verdict read from what it says');
+ } catch (e) {
+ console.error(e);
+ process.exit(1);
+ }
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,237 @@
@@ -0,0 +1,237 @@
+'use strict';
+
+/**
+ * Stage C8, the machine half: the proof adapters (harness/proof.js) with fakes for every effect.
+ *
+ * No command runs, no browser opens and no network is touched: the command runner, Playwright, the site and fetch are
+ * all fakes, so what is tested is the adapter's own reasoning — the diff's shape, the change going to a site instead
+ * of a file, the overflow and the console errors, the reviewer's family, and a token that is never required to try.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const {
+ diffAdapter, screenshotAdapter, reviewAdapter, waitForSite, freePort,
+} = require('../harness/proof');
+const { check, done } = require('./helpers/boot');
+
+/** A fake Playwright: one page per viewport, with a scroll width, console errors and page errors to report. */
+function fakeBrowser({ scrollWidth = null, consoleErrors = [], pageErrors = [], finalUrl = null } = {}) {
+ const shots = [];
+ const launched = [];
+ return {
+ shots, launched,
+ chromium: {
+ async launch(options) {
+ launched.push(options);
+ return {
+ async close() {},
+ async newPage(options) {
+ const width = options.viewport.width;
+ let at = null;
+ const handlers = {};
+ return {
+ on(name, fn) { (handlers[name] = handlers[name] || []).push(fn); },
+ async goto(url) {
+ at = url;
+ for (const text of consoleErrors) {
+ for (const fn of handlers.console || []) fn({ type: () => 'error', text: () => text });
+ }
+ for (const text of pageErrors) {
+ for (const fn of handlers.pageerror || []) fn(new Error(text));
+ }
+ },
+ async waitForTimeout() {},
+ async evaluate() { return scrollWidth == null ? width - 1 : scrollWidth; },
+ async screenshot({ path: file }) { shots.push({ path: file, width, url: at }); },
+ url() { return finalUrl || at; },
+ };
+ },
+ };
+ },
+ },
+ };
+}
+
+/** A fake command runner: git's three questions, answered. */
+function fakeRun(answers = {}) {
+ const calls = [];
+ const run = async (argv) => {
+ calls.push(argv.join(' '));
+ const key = argv.slice(1).join(' ');
+ return { code: 0, output: answers[key] !== undefined ? answers[key] : '' };
+ };
+ run.calls = calls;
+ run.cwd = '/repo';
+ return run;
+}
+
+const OK = async () => ({ status: 200, ok: true, json: async () => ({ choices: [{ message: { content: '{"verdict":"approved","summary":"It holds together."}' } }] }) });
+
+(async () => {
+ try {
+ await check('the diff is read: the files, the stats and the patch', async () => {
+ const run = fakeRun({
+ 'diff --stat HEAD': ' a.js | 3 ++-',
+ 'diff --name-only HEAD': 'a.js\nb.css\n',
+ 'diff --no-color HEAD': '+x\n-y',
+ });
+ const diff = await diffAdapter(run)({ cwd: '/repo' });
+ assert.deepStrictEqual(diff.files, ['a.js', 'b.css']);
+ assert.strictEqual(diff.stats, 'a.js | 3 ++-');
+ assert.strictEqual(diff.patch, '+x\n-y');
+ assert.strictEqual(diff.summary, '2 files changed');
+ });
+
+ await check('a static page is shot from the working tree, at a phone\'s width', async () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-proof-'));
+ fs.writeFileSync(path.join(dir, 'index.html'), '<!doctype html><p>hi</p>');
+ const browser = fakeBrowser();
+ const shot = await screenshotAdapter({
+ run: fakeRun(), playwright: browser, chromePath: '/bin/true', outDir: path.join(dir, 'shots'),
+ })({ cwd: dir, width: 390, files: ['index.html'], routes: ['/'] });
+ assert.strictEqual(shot.ok, true);
+ assert.strictEqual(shot.shots.length, 1);
+ assert.ok(shot.shots[0].url.startsWith('file://'), shot.shots[0].url);
+ });
+
+ await check('a page only the app renders is shot from the repo\'s own site, which is stopped afterwards', async () => {
+ const browser = fakeBrowser();
+ const seen = [];
+ let stopped = 0;
+ const shot = await screenshotAdapter({
+ run: fakeRun(), playwright: browser, chromePath: '/bin/true', outDir: path.join(os.tmpdir(), 'codes-proof-shots'),
+ startSiteFn: async ({ cwd }) => {
+ seen.push(cwd);
+ return { url: 'http://127.0.0.1:4321', stop: async () => { stopped++; } };
+ },
+ })({
+ cwd: '/repo', width: 1440, files: ['server/http/run.js'], routes: ['/'],
+ pkg: { scripts: { start: 'node server/index.js' } },
+ });
+ assert.deepStrictEqual(seen, ['/repo']);
+ assert.strictEqual(stopped, 1, 'the site is stopped even when it answered');
+ assert.strictEqual(shot.ok, true);
+ assert.strictEqual(shot.shots[0].url, 'http://127.0.0.1:4321/');
+ assert.strictEqual(shot.shots[0].live, true);
+ });
+
+ await check('a site that does not start is said out loud, not turned into a clean screenshot', async () => {
+ const browser = fakeBrowser();
+ const shot = await screenshotAdapter({
+ run: fakeRun(), playwright: browser, chromePath: '/bin/true', outDir: path.join(os.tmpdir(), 'codes-proof-shots'),
+ startSiteFn: async () => { const e = new Error('the repo\'s site did not start'); e.code = 'site.not_ready'; throw e; },
+ })({ cwd: '/repo', width: 390, files: ['server/http/run.js'], routes: ['/'], pkg: { scripts: { start: 'node server/index.js' } } });
+ assert.strictEqual(shot.skipped, true);
+ assert.ok(/did not start/.test(shot.summary), shot.summary);
+ });
+
+ await check('a page that scrolls sideways fails the screenshots', async () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-proof-'));
+ fs.writeFileSync(path.join(dir, 'index.html'), '<!doctype html><p>wide</p>');
+ const shot = await screenshotAdapter({
+ run: fakeRun(), playwright: fakeBrowser({ scrollWidth: 480 }), chromePath: '/bin/true', outDir: path.join(dir, 'shots'),
+ })({ cwd: dir, width: 390, files: ['index.html'], routes: ['/'] });
+ assert.strictEqual(shot.ok, false);
+ assert.ok(/scrolls sideways/.test(shot.summary), shot.summary);
+ });
+
+ await check('a page that throws fails the screenshots', async () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-proof-'));
+ fs.writeFileSync(path.join(dir, 'index.html'), '<!doctype html><p>hi</p>');
+ const shot = await screenshotAdapter({
+ run: fakeRun(), playwright: fakeBrowser({ pageErrors: ['TypeError: x is undefined'] }), chromePath: '/bin/true', outDir: path.join(dir, 'shots'),
+ })({ cwd: dir, width: 1440, files: ['index.html'], routes: ['/'] });
+ assert.strictEqual(shot.ok, false);
+ assert.ok(/the page threw/.test(shot.summary), shot.summary);
+ });
+
+ await check('a blocked third-party request is kept as detail, and does not fail the page', async () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codes-proof-'));
+ fs.writeFileSync(path.join(dir, 'index.html'), '<!doctype html><p>hi</p>');
+ const shot = await screenshotAdapter({
+ run: fakeRun(), playwright: fakeBrowser({ consoleErrors: ['Failed to load resource: net::ERR_FAILED'] }), chromePath: '/bin/true', outDir: path.join(dir, 'shots'),
+ })({ cwd: dir, width: 390, files: ['index.html'], routes: ['/'] });
+ assert.strictEqual(shot.ok, true);
+ assert.deepStrictEqual(shot.shots[0].console_errors, ['Failed to load resource: net::ERR_FAILED']);
+ });
+
+ await check('a machine with no browser says it cannot shoot, instead of failing', async () => {
+ const shot = await screenshotAdapter({ run: fakeRun(), playwright: null, chromePath: null, outDir: '/tmp/x' })({ files: ['index.html'] });
+ assert.strictEqual(shot.skipped, true);
+ assert.ok(/no browser/.test(shot.summary), shot.summary);
+ });
+
+ await check('the review needs the task\'s token, and says so when it has none', async () => {
+ const review = reviewAdapter({ env: {}, fetchImpl: OK, models: ['deepseek-chat'] });
+ await assert.rejects(() => review({ patch: '+x', family: 'anthropic' }), (e) => e.code === 'review.no_token');
+ });
+
+ await check('the review goes to OpenVibe.AI as chat completions, with the token', async () => {
+ const calls = [];
+ const fetchImpl = async (url, options) => { calls.push({ url, options }); return OK(); };
+ const review = reviewAdapter({ env: { OPENVIBE_CODES_TASK_TOKEN: 'test-token' }, fetchImpl, models: ['deepseek-chat'] });
+ const verdict = await review({ patch: '+x', summary: 'a change', family: 'anthropic', model: 'deepseek-chat' });
+ assert.strictEqual(verdict.verdict, 'approved');
+ assert.strictEqual(verdict.family, 'deepseek');
+ assert.ok(calls[0].url.endsWith('/chat/completions'), calls[0].url);
+ assert.strictEqual(calls[0].options.headers.authorization, 'Bearer test-token');
+ });
+
+ await check('the review spends the run token the attempt carries, at the endpoint that token names', async () => {
+ const calls = [];
+ const fetchImpl = async (url, options) => { calls.push({ url, options }); return OK(); };
+ const review = reviewAdapter({
+ env: { OPENAI_API_KEY: 'a-personal-key' },
+ jobEnv: { ANTHROPIC_BASE_URL: 'https://ai.openvibe.services', ANTHROPIC_AUTH_TOKEN: 'run-token' },
+ fetchImpl,
+ });
+ const verdict = await review({ patch: '+x', family: 'anthropic', model: 'deepseek-chat' });
+ assert.strictEqual(verdict.verdict, 'approved');
+ assert.strictEqual(calls[0].url, 'https://ai.openvibe.services/v1/chat/completions');
+ assert.strictEqual(calls[0].options.headers.authorization, 'Bearer run-token');
+ });
+
+ await check('a personal provider key in the environment is not spent as a run token', async () => {
+ const review = reviewAdapter({ env: { OPENAI_API_KEY: 'a-personal-key' }, fetchImpl: OK, models: ['deepseek-chat'] });
+ await assert.rejects(() => review({ patch: '+x', family: 'anthropic' }), (e) => e.code === 'review.no_token');
+ });
+
+ await check('a provider that speaks the messages dialect is tried when chat completions is not there', async () => {
+ const calls = [];
+ const fetchImpl = async (url) => {
+ calls.push(url);
+ if (url.endsWith('/chat/completions')) return { status: 404, ok: false, json: async () => ({}) };
+ return { status: 200, ok: true, json: async () => ({ content: [{ type: 'text', text: 'changes requested: the parser still throws' }] }) };
+ };
+ const review = reviewAdapter({ env: { OPENVIBE_AI_TOKEN: 'test-token' }, fetchImpl });
+ const verdict = await review({ patch: '+x', family: 'anthropic', model: 'claude-sonnet-4' });
+ assert.ok(calls[1].endsWith('/messages'), calls[1]);
+ assert.strictEqual(verdict.verdict, 'changes_requested');
+ });
+
+ await check('the site readiness probe waits through a failure and accepts a redirect', async () => {
+ let tries = 0;
+ const fetchImpl = async () => {
+ tries++;
+ if (tries < 3) throw new Error('ECONNREFUSED');
+ return { status: 302 };
+ };
+ const ready = await waitForSite('http://127.0.0.1:1', { fetchImpl, timeoutMs: 5000, everyMs: 5 });
+ assert.strictEqual(ready.ok, true);
+ assert.strictEqual(tries, 3);
+ });
+
+ await check('a free port is a port a site can answer on', async () => {
+ const port = await freePort();
+ assert.ok(Number.isInteger(port) && port > 0 && port < 65536, String(port));
+ });
+
+ console.log('harness proof: the diff, the pages (file or site), the overflow and the second opinion, with fakes');
+ } catch (e) {
+ console.error(e);
+ process.exit(1);
+ }
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,169 @@
@@ -0,0 +1,169 @@
+'use strict';
+
+/**
+ * Stage C8: a finished task's proof. The machine collects it (harness/job.js) and sends it with the result; the
+ * server keeps it (one row per item), tells the world one event per item, and says the task's outcome in plain words.
+ *
+ * The review is behind an interface (engine/review.js): with the connections of stage C3 it is a model of another
+ * family through OpenVibe.AI; without them it is a fake, and the fake is what this test uses.
+ */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/boot');
+
+(async () => {
+ const jobs = new Map();
+ let counter = 0;
+ const run = {
+ enabled: true,
+ async submit(o) {
+ counter++;
+ const id = `job_01J8Z3Q4R5S6T7V8W9X0Y1Z2J${String(counter).padStart(2, '0')}`;
+ jobs.set(id, { id, state: 'queued', args: o.args });
+ return { id, state: 'queued' };
+ },
+ async read(jobId) { return jobs.get(jobId) || { id: jobId, state: 'queued' }; },
+ async cancel() { throw new Error('not used'); },
+ async streamUrl() { throw new Error('not used'); },
+ };
+ const t = await boot({ run });
+ const db = t.ctx.store.db;
+ const ada = t.network.addUser('ada');
+ const project = 'prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2P1';
+
+ // A service's task runs in the project of the person's latest /run task, so Ada needs one before she starts any.
+ await db.exec(`INSERT INTO run_tasks (id, subject, project_id, job_id, repo, prompt, agent, state, created_at, updated_at)
+ VALUES ($1, $2, $3, 'job_01J8Z3Q4R5S6T7V8W9X0Y1Z2J98', 'https://github.com/a/b', 'an earlier task', 'claude-code', 'succeeded', 1, 2)`,
+ ['tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2T90', ada.subject, project]);
+
+ const CAPS = ['codes.task.start', 'codes.task.read', 'codes.task.cancel'];
+ const actor = t.network.mintService('svc:actor', 'openvibe.codes', CAPS);
+ const startTask = async (prompt = 'Fix the parser and add a test for it.') => {
+ const res = await t.get('/api/v1/tasks', {
+ method: 'POST', as: ada, json: { repo: 'https://github.com/octocat/Hello-World', prompt, idempotency_key: `act_${Math.random().toString(36).slice(2, 10)}` },
+ headers: { Authorization: `Bearer ${actor}`, 'X-OV-Subject': ada.subject },
+ });
+ assert.strictEqual(res.status, 201, res.text.slice(0, 300));
+ return res.json().task.id;
+ };
+
+ /** The attempt ends with a result that carries the proof the machine collected. */
+ const endAttempt = async (taskId, { state = 'succeeded', result = null } = {}) => {
+ const row = await db.one('SELECT * FROM run_tasks WHERE id = $1', [taskId]);
+ const job = { id: row.job_id, state, result, error: null };
+ const updated = await t.ctx.tasks.applyEvent(row, { state, job });
+ await t.ctx.tasks.afterEnd(updated.id);
+ return await db.one('SELECT * FROM run_tasks WHERE id = $1', [taskId]);
+ };
+
+ /** The proof a machine would collect: the tests, the pages, the diff and the review. */
+ const proofOf = (items = null) => ({
+ items: items || [
+ { kind: 'tests', label: 'tests', ok: true, skipped: false, summary: 'only the tests the change reaches (proof)' },
+ { kind: 'screenshots', label: 'screenshots', ok: true, skipped: false, summary: '2 screenshots of 1 page' },
+ { kind: 'diff', label: 'diff', ok: true, skipped: false, summary: '1 file changed' },
+ { kind: 'review', label: 'review', ok: true, skipped: false, family: 'deepseek', summary: 'It holds together.' },
+ ],
+ outcome: { state: 'done', note: 'Done with proof.' },
+ });
+
+ const proofRows = async (taskId) => await db.many('SELECT * FROM codes_task_proof WHERE task_id = $1 ORDER BY n ASC', [taskId]);
+ const proofLines = async (taskId) => await db.many("SELECT seq, kind, body FROM codes_session_lines WHERE task_id = $1 AND kind = 'proof' ORDER BY seq", [taskId]);
+
+ try {
+ await check('a finished task keeps its proof: one row per item, in order', async () => {
+ const id = await startTask();
+ await endAttempt(id, { result: { ok: true, summary: 'done', proof: proofOf() } });
+ const rows = await proofRows(id);
+ assert.strictEqual(rows.length, 4);
+ assert.deepStrictEqual(rows.map((r) => r.kind), ['tests', 'screenshots', 'diff', 'review']);
+ assert.strictEqual(rows[3].family, 'deepseek');
+ assert.strictEqual(rows[0].attempt_n, 1, 'the items belong to the attempt that ended');
+ });
+
+ await check('every proof item is an event on the task\'s stream', async () => {
+ const id = await startTask();
+ await endAttempt(id, { result: { ok: true, proof: proofOf() } });
+ const lines = await proofLines(id);
+ assert.strictEqual(lines.length, 4);
+ assert.strictEqual(lines[0].body.t, 'proof');
+ assert.strictEqual(lines[0].body.kind, 'tests');
+ assert.strictEqual(lines[3].body.family, 'deepseek');
+ assert.ok(lines.every((l, i) => i === 0 || Number(l.seq) > Number(lines[i - 1].seq)), 'the lines are in order');
+ });
+
+ await check('the outcome is in plain words: done with proof', async () => {
+ const id = await startTask();
+ const row = await endAttempt(id, { result: { ok: true, proof: proofOf() } });
+ assert.strictEqual(row.outcome, 'done');
+ assert.ok(/^Done with proof/.test(row.outcome_note), row.outcome_note);
+ });
+
+ await check('a proof that failed is said out loud, and the task is not called done', async () => {
+ const id = await startTask();
+ const row = await endAttempt(id, {
+ result: { ok: true, proof: proofOf([
+ { kind: 'tests', label: 'tests', ok: false, skipped: false, summary: 'FAIL test/proof.test.js' },
+ { kind: 'diff', label: 'diff', ok: true, skipped: false, summary: '1 file changed' },
+ ]) },
+ });
+ assert.strictEqual(row.outcome, 'unfinished', 'the evidence wins over the agent\'s claim');
+ assert.ok(/Didn't finish, with what is left/.test(row.outcome_note), row.outcome_note);
+ assert.ok(/FAIL test/.test(row.outcome_note), row.outcome_note);
+ });
+
+ await check('a task the person stopped stays stopped, whatever the proof says', async () => {
+ const id = await startTask();
+ const row = await endAttempt(id, {
+ state: 'cancelled',
+ result: { ok: false, proof: proofOf([{ kind: 'tests', label: 'tests', ok: false, skipped: false, summary: 'FAIL' }]) },
+ });
+ assert.strictEqual(row.outcome, 'stopped');
+ });
+
+ await check('a task without a proof says nothing about one', async () => {
+ const id = await startTask();
+ const row = await endAttempt(id, { result: { ok: true, summary: 'done' } });
+ assert.strictEqual((await proofRows(id)).length, 0);
+ assert.ok(row.outcome_note, 'the engine\'s own note still stands');
+ });
+
+ await check('the API answers codes.task@2 with the outcome in plain words', async () => {
+ const id = await startTask();
+ await endAttempt(id, { result: { ok: true, proof: proofOf() } });
+ const res = await t.get(`/api/v1/tasks/${id}`, { as: ada });
+ assert.strictEqual(res.status, 200, res.text.slice(0, 200));
+ const doc = res.json();
+ assert.strictEqual(doc.outcome.state, 'done');
+ assert.ok(/^Done with proof/.test(doc.outcome.note), doc.outcome.note);
+ });
+
+ await check('recording the same proof twice makes no second event', async () => {
+ const id = await startTask();
+ const row = await db.one('SELECT * FROM run_tasks WHERE id = $1', [id]);
+ const job = { id: row.job_id, state: 'succeeded', result: { ok: true, proof: proofOf() } };
+ await t.ctx.tasks.applyEvent(row, { state: 'succeeded', job });
+ await t.ctx.tasks.afterEnd(row.id);
+ await t.ctx.tasks.applyEvent(row, { state: 'succeeded', job });
+ await t.ctx.tasks.afterEnd(row.id);
+ assert.strictEqual((await proofRows(id)).length, 4);
+ assert.ok((await proofLines(id)).length <= 8, 'the lines do not pile up');
+ });
+
+ await check('the task page shows the proof, each item with what it found', async () => {
+ const id = await startTask();
+ await endAttempt(id, { result: { ok: true, proof: proofOf() } });
+ const res = await t.get(`/run/tasks/${id}`, { as: ada });
+ assert.strictEqual(res.status, 200, res.text.slice(0, 200));
+ assert.ok(res.text.includes('data-proof'), 'the page has the proof section');
+ assert.ok(res.text.includes('data-proof-kind="tests"'), 'the tests are listed');
+ assert.ok(res.text.includes('data-proof-kind="review"'), 'the review is listed');
+ assert.ok(res.text.includes('deepseek'), 'the reviewer\'s family is shown');
+ });
+
+ console.log('proof: the items are kept, each one is an event, and the outcome is in plain words');
+ } catch (e) {
+ console.error(e);
+ process.exit(1);
+ }
+ done();
+})();