From 74b30a8ddcfeb2e8b2e722aab588e183a9df5fd4 Mon Sep 17 00:00:00 2001 From: mgillr <36207807+mgillr@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:04:32 +0000 Subject: [PATCH 1/6] I point the package at Optitransfer/proofline and run the signature test on the agentkey devDependency The repository moved to the Optitransfer org; homepage, repository and bugs follow it. The owner-signature test ran agentkey from a path on my laptop, so it failed in any other clone. It now uses @optitransfer/agentkey from the lockfile, and CI installs that lockfile before the self-gate. (PL-SELF) Signed-off-by: mgillr <36207807+mgillr@users.noreply.github.com> --- .github/workflows/proofline.yml | 2 ++ package-lock.json | 22 ++++++++++++++++++++++ package.json | 7 ++++--- test/contract_sig.test.mjs | 3 ++- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/proofline.yml b/.github/workflows/proofline.yml index 741b115..8185258 100644 --- a/.github/workflows/proofline.yml +++ b/.github/workflows/proofline.yml @@ -16,6 +16,8 @@ jobs: with: node-version: 22 - run: git config --global user.email ci@proofline.local && git config --global user.name ci + - name: Install the pinned dev dependencies (agentkey, hivevault) from the committed lockfile + run: npm ci --ignore-scripts - name: Proofline self-gate (independent re-verification; hooks cannot be skipped here) # A fresh checkout has no pinned base and, after a push, origin/main is HEAD: gate against the previous tip. run: node bin/proofline.mjs gate --ci ${{ github.event_name == 'push' && format('--base {0}', github.event.before) || '' }} diff --git a/package-lock.json b/package-lock.json index 9576c33..96df03d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,34 @@ "proofline": "bin/proofline.mjs" }, "devDependencies": { + "@optitransfer/agentkey": "0.1.1", "hivevault": "0.3.3" }, "engines": { "node": ">=22" } }, + "node_modules/@optitransfer/agentkey": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@optitransfer/agentkey/-/agentkey-0.1.1.tgz", + "integrity": "sha512-09+MOpbtsQULWj+GaTxV+WrF2M9QgQ49BEBWw7sJnMe/t8fgYkC2S4T89qOEwFjhKSFUb/Tj4i0BBFoGjpRpXA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "agentkey": "bin/agentkey.mjs" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@sigstore/sign": "3.1.0" + }, + "peerDependenciesMeta": { + "@sigstore/sign": { + "optional": true + } + } + }, "node_modules/hivevault": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/hivevault/-/hivevault-0.3.3.tgz", diff --git a/package.json b/package.json index 014a076..936a85b 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,13 @@ "nodejs", "cli" ], - "homepage": "https://github.com/mgillr/proofline", + "homepage": "https://github.com/Optitransfer/proofline", "repository": { "type": "git", - "url": "git+https://github.com/mgillr/proofline.git" + "url": "git+https://github.com/Optitransfer/proofline.git" }, "bugs": { - "url": "https://github.com/mgillr/proofline/issues" + "url": "https://github.com/Optitransfer/proofline/issues" }, "author": "mgillr", "license": "Apache-2.0", @@ -57,6 +57,7 @@ "mcp": "node bin/proofline.mjs mcp" }, "devDependencies": { + "@optitransfer/agentkey": "0.1.1", "hivevault": "0.3.3" } } diff --git a/test/contract_sig.test.mjs b/test/contract_sig.test.mjs index 99c2746..7088f49 100644 --- a/test/contract_sig.test.mjs +++ b/test/contract_sig.test.mjs @@ -7,7 +7,8 @@ import assert from 'node:assert/strict'; import * as P from '../src/index.mjs'; import { repo, commitBeforeBase, BIN } from './helpers.mjs'; -const AGENTKEY = '/Users/imac27/programme-b/agentkey/bin/agentkey.mjs'; +// The real agentkey CLI from the @optitransfer/agentkey devDependency, so this test runs in any clone and in CI. +const AGENTKEY = path.resolve(import.meta.dirname, '../node_modules/@optitransfer/agentkey/bin/agentkey.mjs'); function agentEnv() { const home = fs.mkdtempSync(path.join(os.tmpdir(), 'pl-home-')); From f2724cfa916419b6aa217a3777a06baa14783d6d Mon Sep 17 00:00:00 2001 From: mgillr <36207807+mgillr@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:04:32 +0000 Subject: [PATCH 2/6] I make setup and contract sign say how to provide AGENTKEY_HOME and agentkey Both refusals ended in 'Next step: proofline setup', which sent the user round in a circle. Each now says what the variable or command is for and prints the exact command that provides it. (PL-SELF) Signed-off-by: mgillr <36207807+mgillr@users.noreply.github.com> --- src/guided.mjs | 18 ++++++++--- src/signature.mjs | 4 +-- test/setup_guidance.test.mjs | 59 ++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 test/setup_guidance.test.mjs diff --git a/src/guided.mjs b/src/guided.mjs index 0edf230..c8206ae 100644 --- a/src/guided.mjs +++ b/src/guided.mjs @@ -16,6 +16,16 @@ const WATCH_INTERVAL_MS = 1000; const refuse = (why, next) => new Error(`${why}\nNext step: ${next}`); +// setup needs two things from the shell. Each refusal says what it is for and the exact commands that provide it. +const noKeyHome = () => refuse( + 'AGENTKEY_HOME is not set, so no key directory was opened. It names the directory that holds your owner key; proofline never picks one for you', + 'export AGENTKEY_HOME="$HOME/.proofline-owner" (add it to your shell profile, contract sign reads it too), then run proofline setup again', +); +const noAgentkey = () => refuse( + 'agentkey is not on PATH. proofline setup creates the owner key with agentkey (@optitransfer/agentkey)', + 'npm install --save-dev @optitransfer/agentkey and run npx proofline setup, or npm install -g @optitransfer/agentkey, then run proofline setup again', +); + function pause(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } @@ -50,9 +60,9 @@ function hooksReady(root) { function createOwnerKey(env) { const home = env.AGENTKEY_HOME; - if (typeof home !== 'string' || !home.trim()) throw refuse('AGENTKEY_HOME is not set, so no key directory was opened', 'proofline setup'); + if (typeof home !== 'string' || !home.trim()) throw noKeyHome(); const bin = agentkeyBin(env); - if (!bin) throw refuse('agentkey is not on PATH', 'proofline setup'); + if (!bin) throw noAgentkey(); const r = spawnSync(bin, ['init', 'owner'], { encoding: 'utf8', env: { ...env, AGENTKEY_HOME: home } }); if (r.error || r.status !== 0) throw refuse('agentkey init owner failed', 'proofline setup'); const key = path.join(home, 'keys', 'owner.key'); @@ -111,8 +121,8 @@ export function setup(root, { agent = 'none', env = process.env } = {}) { if (agent !== 'none' && agent !== 'claude-code') throw refuse(`unknown agent ${agent}`, 'proofline setup --agent none'); if (!git(['rev-parse', '--show-toplevel'], root).ok) throw refuse('not a git repository', 'git init'); const approvals = path.resolve(root, '.proofline', 'approvals'); - if (typeof env.AGENTKEY_HOME !== 'string' || !env.AGENTKEY_HOME.trim()) throw refuse('AGENTKEY_HOME is not set, so no key directory was opened', 'proofline setup'); - if (!agentkeyBin(env)) throw refuse('agentkey is not on PATH', 'proofline setup'); + if (typeof env.AGENTKEY_HOME !== 'string' || !env.AGENTKEY_HOME.trim()) throw noKeyHome(); + if (!agentkeyBin(env)) throw noAgentkey(); checkOwnerPin(root, env); try { init(root, { hooks: true, approvals }); diff --git a/src/signature.mjs b/src/signature.mjs index d493502..06f2ac5 100644 --- a/src/signature.mjs +++ b/src/signature.mjs @@ -72,10 +72,10 @@ export function verifyContractSignature(root) { export function signContract(root, env = process.env) { const home = env.AGENTKEY_HOME; if (typeof home !== 'string' || !home.trim()) { - throw new Error('AGENTKEY_HOME is not set, so no key directory was opened\nNext step: proofline contract sign'); + throw new Error('AGENTKEY_HOME is not set, so no key directory was opened. It names the directory that holds the owner key proofline setup created\nNext step: export AGENTKEY_HOME=, then run proofline contract sign again'); } const bin = agentkeyBin(env); - if (!bin) throw new Error('agentkey is not on PATH\nNext step: proofline contract sign'); + if (!bin) throw new Error('agentkey is not on PATH. The owner signature is made with agentkey (@optitransfer/agentkey)\nNext step: npm install --save-dev @optitransfer/agentkey and run npx proofline contract sign, or npm install -g @optitransfer/agentkey, then run proofline contract sign again'); const hash = canonicalHash(root); const r = spawnSync(bin, ['sign', 'owner', '--type', PAYLOAD_TYPE], { input: hash, encoding: 'utf8', env: { ...env, AGENTKEY_HOME: home }, diff --git a/test/setup_guidance.test.mjs b/test/setup_guidance.test.mjs new file mode 100644 index 0000000..cb0711d --- /dev/null +++ b/test/setup_guidance.test.mjs @@ -0,0 +1,59 @@ +// proofline setup and contract sign name the exact commands that provide what they need, never just "run setup again". +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync, spawnSync } from 'node:child_process'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { BIN } from './helpers.mjs'; + +function emptyRepo() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pl-guide-')); + execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: root }); + return root; +} + +// A PATH that holds git and nothing named agentkey, whatever the machine has installed. +function pathWithoutAgentkey() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pl-path-')); + const git = execFileSync('sh', ['-c', 'command -v git'], { encoding: 'utf8' }).trim(); + fs.symlinkSync(git, path.join(dir, 'git')); + return dir; +} + +function run(args, env) { + return spawnSync(process.execPath, [BIN, ...args], { encoding: 'utf8', env }); +} + +test('setup without AGENTKEY_HOME says what it is for and how to set it', () => { + const root = emptyRepo(); + const env = { ...process.env }; + delete env.AGENTKEY_HOME; + const r = run(['setup', '--root', root], env); + assert.notEqual(r.status, 0); + assert.match(r.stderr, /AGENTKEY_HOME is not set/); + assert.match(r.stderr, /holds your owner key/); + assert.match(r.stderr, /Next step: export AGENTKEY_HOME=/); + assert.equal(fs.existsSync(path.join(root, 'proofline.json')), false); +}); + +test('setup without agentkey on PATH names the package and the install command', () => { + const root = emptyRepo(); + const keyHome = fs.mkdtempSync(path.join(os.tmpdir(), 'pl-ak-')); + const env = { ...process.env, AGENTKEY_HOME: keyHome, PATH: pathWithoutAgentkey() }; + const r = run(['setup', '--root', root], env); + assert.notEqual(r.status, 0); + assert.match(r.stderr, /agentkey is not on PATH/); + assert.match(r.stderr, /npm install --save-dev @optitransfer\/agentkey/); + assert.equal(fs.existsSync(path.join(root, 'proofline.json')), false); +}); + +test('contract sign without AGENTKEY_HOME names the variable to export', () => { + const root = emptyRepo(); + const env = { ...process.env }; + delete env.AGENTKEY_HOME; + const r = run(['contract', 'sign', '--root', root], env); + assert.notEqual(r.status, 0); + assert.match(r.stderr, /AGENTKEY_HOME is not set/); + assert.match(r.stderr, /Next step: export AGENTKEY_HOME=/); +}); From 237d59ab9105106157bbea0078b694a5b94dba4f Mon Sep 17 00:00:00 2001 From: mgillr <36207807+mgillr@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:04:32 +0000 Subject: [PATCH 3/6] I add proofline --version (PL-SELF) Signed-off-by: mgillr <36207807+mgillr@users.noreply.github.com> --- bin/proofline.mjs | 7 +++++++ test/version.test.mjs | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 test/version.test.mjs diff --git a/bin/proofline.mjs b/bin/proofline.mjs index 8dc3301..483b62e 100755 --- a/bin/proofline.mjs +++ b/bin/proofline.mjs @@ -18,6 +18,12 @@ for (let i = 0; i < argv.length; i++) { if (a.startsWith('--')) { const [k, v] = a.slice(2).split('='); if (v !== undefined) flags[k] = v; else if (!SWITCHES.includes(k) && argv[i + 1] && !argv[i + 1].startsWith('--')) flags[k] = argv[++i]; else flags[k] = true; } else pos.push(a); } +// --version (or -v) on its own prints the installed package version and exits 0, before any repository is read. +if ((pos.length === 0 && flags.version === true && Object.keys(flags).length === 1) || (argv.length === 1 && argv[0] === '-v')) { + const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')); + process.stdout.write(`${pkg.version}\n`); + process.exit(0); +} const root = flags.root ? flags.root : P.findRoot(); const json = !!flags.json; const print = (obj, text) => { process.stdout.write((json || text === undefined ? JSON.stringify(obj, null, 2) : text) + '\n'); }; @@ -73,6 +79,7 @@ const HELP = `proofline: independent V&V and delivery controller hook stop read a Stop hook payload on stdin; one receipt for new commits, or silence connect claude-code merge a Stop hook into ~/.claude/settings.json, after any hook already there mcp MCP server on stdio + --version print the installed version Global: --json --root Exit: 0 PASS, 1 FAIL, 2 BLOCKED, 3 usage`; const GLOBAL_FLAGS = ['json', 'root']; diff --git a/test/version.test.mjs b/test/version.test.mjs new file mode 100644 index 0000000..dc7eaa8 --- /dev/null +++ b/test/version.test.mjs @@ -0,0 +1,25 @@ +// proofline --version prints the package version, so a user can say which build they run. +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { BIN } from './helpers.mjs'; + +const VERSION = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, '..', 'package.json'), 'utf8')).version; + +test('--version and -v print the package version and exit 0, outside any repository', () => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'pl-ver-')); + for (const flag of ['--version', '-v']) { + const r = spawnSync(process.execPath, [BIN, flag], { encoding: 'utf8', cwd }); + assert.equal(r.status, 0, r.stderr); + assert.equal(r.stdout, `${VERSION}\n`); + } +}); + +test('--version on a subcommand is still an unknown flag', () => { + const r = spawnSync(process.execPath, [BIN, 'status', '--version'], { encoding: 'utf8' }); + assert.equal(r.status, 2); + assert.match(r.stderr, /unknown flag --version/); +}); From 1b9b023247b2c80f73139885ec4aa4c3cfb3ae48 Mon Sep 17 00:00:00 2001 From: mgillr <36207807+mgillr@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:04:32 +0000 Subject: [PATCH 4/6] I word AGENTS.md so a copied file passes a fresh contract's forbidden scan The README tells users to copy AGENTS.md into their repository. Its 'Never' list wrote the hook-skipping flag literally, so the first verify failed on no_verify. (PL-SELF) Signed-off-by: mgillr <36207807+mgillr@users.noreply.github.com> --- AGENTS.md | 2 +- test/agents_md_scan.test.mjs | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 test/agents_md_scan.test.mjs diff --git a/AGENTS.md b/AGENTS.md index 5e07feb..183c076 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Proofline rejects: "done" without a commit, test names or measured numbers; numb - Skip/only/todo tests, raise timeouts, delete tests or assertions, edit fixtures to pass (unless the fixture is the proven root cause, with proof). - Lower a target, cap or test size, or touch thresholds/commands/criteria in `proofline.json` (detected; BLOCKED until the owner approves). -- `--no-verify`, bypass env vars, skip flags. There is no bypass. If CI runs `proofline gate --ci`, it re-runs the gate. +- Skip the git hooks (the no-verify flag on commit or push), set bypass env vars or pass skip flags. There is no bypass. If CI runs `proofline gate --ci`, it re-runs the gate. - Say "done" for anything that is not PASS on the pushed HEAD. - Defer. Only the owner approves a deferral. diff --git a/test/agents_md_scan.test.mjs b/test/agents_md_scan.test.mjs new file mode 100644 index 0000000..c76cd45 --- /dev/null +++ b/test/agents_md_scan.test.mjs @@ -0,0 +1,25 @@ +// The AGENTS.md the README tells users to copy must pass the forbidden scan of a freshly initialised contract. +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import * as P from '../src/index.mjs'; +import { scanForbidden } from '../src/forbidden.mjs'; + +test('copying the shipped AGENTS.md into a repository trips no forbidden rule', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pl-agents-')); + const git = (...a) => execFileSync('git', a, { cwd: root, encoding: 'utf8' }).trim(); + git('init', '-q', '-b', 'main'); + git('config', 'user.email', 't@example.invalid'); + git('config', 'user.name', 't'); + fs.writeFileSync(path.join(root, 'README.md'), 'x\n'); + git('add', '-A'); + git('commit', '-q', '-m', 'base'); + const base = git('rev-parse', 'HEAD'); + fs.copyFileSync(path.resolve(import.meta.dirname, '..', 'AGENTS.md'), path.join(root, 'AGENTS.md')); + git('add', '-A'); + const rules = P.C.skeleton('t').forbidden; + assert.deepEqual(scanForbidden(root, base, rules), []); +}); From d0ccc4e4a9bc7af5385333bc50532db0236647d0 Mon Sep 17 00:00:00 2001 From: mgillr <36207807+mgillr@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:04:32 +0000 Subject: [PATCH 5/6] I name the issue each regression test covers: #21, #22, #23, #31, #33, #35 (PL-RV15) Signed-off-by: mgillr <36207807+mgillr@users.noreply.github.com> --- test/cli_unknown_flags.test.mjs | 1 + test/hook_base_repin.test.mjs | 1 + test/hook_fresh_worktree.test.mjs | 1 + test/hook_push_range.test.mjs | 1 + test/hook_push_sha.test.mjs | 1 + 5 files changed, 5 insertions(+) diff --git a/test/cli_unknown_flags.test.mjs b/test/cli_unknown_flags.test.mjs index 72c9516..c3cf420 100644 --- a/test/cli_unknown_flags.test.mjs +++ b/test/cli_unknown_flags.test.mjs @@ -1,3 +1,4 @@ +// Regression for #33: every subcommand refuses a flag it does not accept. import test from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; diff --git a/test/hook_base_repin.test.mjs b/test/hook_base_repin.test.mjs index 7c0709f..734678f 100644 --- a/test/hook_base_repin.test.mjs +++ b/test/hook_base_repin.test.mjs @@ -1,3 +1,4 @@ +// Regression for #22: a later start moves the pinned base. import test from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; diff --git a/test/hook_fresh_worktree.test.mjs b/test/hook_fresh_worktree.test.mjs index 0ca54ec..cac6173 100644 --- a/test/hook_fresh_worktree.test.mjs +++ b/test/hook_fresh_worktree.test.mjs @@ -1,3 +1,4 @@ +// Regression for #23: a push from a worktree with no task evidence is still gated. import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; diff --git a/test/hook_push_range.test.mjs b/test/hook_push_range.test.mjs index 4620293..50bd74e 100644 --- a/test/hook_push_range.test.mjs +++ b/test/hook_push_range.test.mjs @@ -1,3 +1,4 @@ +// Regressions for #31 (a rebased branch re-gated tasks already on main) and #35 (a forged remote-tracking ref skipped the gate). import test from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; diff --git a/test/hook_push_sha.test.mjs b/test/hook_push_sha.test.mjs index 4dd7069..0b0135f 100644 --- a/test/hook_push_sha.test.mjs +++ b/test/hook_push_sha.test.mjs @@ -1,3 +1,4 @@ +// Regression for #21: the pre-push hook gates the commit being pushed, not the checked-out HEAD. import test from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; From 9f77682dea4364a1dc9b967d1b09e17ba194b3e2 Mon Sep 17 00:00:00 2001 From: mgillr <36207807+mgillr@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:04:32 +0000 Subject: [PATCH 6/6] I add the Proofline mark and badges, document setup's two needs, and correct the published version The README named 0.2.0-next.0; npm has 0.2.0-next.1. (PL-SELF) Signed-off-by: mgillr <36207807+mgillr@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ README.md | 22 +++++++++++++++++++++- docs/logo-128.png | Bin 0 -> 2573 bytes docs/logo-512.png | Bin 0 -> 13490 bytes docs/logo-white.svg | 1 + docs/logo.svg | 1 + 6 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 docs/logo-128.png create mode 100644 docs/logo-512.png create mode 100644 docs/logo-white.svg create mode 100644 docs/logo.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index fca52c6..1fb9db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable project changes are recorded here. Dates use ISO 8601 (`YYYY-MM-DD`). +## Unreleased + +### Added +- `proofline --version` (and `-v`) prints the installed version and exits 0. On a subcommand `--version` is still an unknown flag. +- The README and the package carry the Proofline mark (`docs/logo.svg`, `docs/logo-white.svg`, PNGs at 512 and 128). + +### Fixed +- `proofline setup` no longer ends in "Next step: proofline setup" when `AGENTKEY_HOME` is unset or `agentkey` is not on PATH. Each refusal says what the variable or the command is for and prints the exact command that provides it (`export AGENTKEY_HOME=...`, `npm install --save-dev @optitransfer/agentkey`). `proofline contract sign` does the same. The README documents both before the short path. +- The `AGENTS.md` the README tells you to copy no longer trips the `no_verify` rule of a fresh contract on the first `verify`: its "Never" list names the hook-skipping flag in words. +- The owner-signature test ran agentkey from a path on one laptop. It now runs the `@optitransfer/agentkey` devDependency, and CI installs the lockfile with `npm ci --ignore-scripts` before the self-gate. +- The repository moved to `github.com/Optitransfer/proofline`; `homepage`, `repository` and `bugs` in package.json point there. The old URL redirects. + ## 0.2.0-next.1 (2026-10-17) ### Added diff --git a/README.md b/README.md index c14150e..a4a5d2b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ +

+ + + Proofline + +

+ # Proofline +[![proofline](https://github.com/Optitransfer/proofline/actions/workflows/proofline.yml/badge.svg)](https://github.com/Optitransfer/proofline/actions/workflows/proofline.yml) [![npm](https://img.shields.io/npm/v/@optitransfer/proofline/next?label=npm%40next)](https://www.npmjs.com/package/@optitransfer/proofline) + **The agent says done. Proofline verifies and validates the work against a V&V contract, to assure agent performance.** A coding agent reports that a task is finished. Proofline does not take the report: it holds the definition of done as a contract a machine can check, hands the agent one task at a time, re-runs every check itself when the agent reports, and lets a commit through only when the evidence says PASS. The agent gets a corrective prompt that names the file and the line to fix next, never a verdict it can argue with. Zero dependencies, Node 22+, Apache-2.0. @@ -116,7 +125,7 @@ PROOFLINE CORRECTIVE INSTRUCTION for TASK-1 (Reject an empty body): gate FAIL DECISION: FAIL ``` -Exit codes: `0` PASS, `1` FAIL, `2` BLOCKED, `3` usage. Every command takes `--json`. The published package is `@optitransfer/proofline` at `0.2.0-next.0` on the `next` dist-tag: `npx @optitransfer/proofline@next`. +Exit codes: `0` PASS, `1` FAIL, `2` BLOCKED, `3` usage. Every command takes `--json`. The published package is `@optitransfer/proofline` at `0.2.0-next.1` on the `next` dist-tag: `npx @optitransfer/proofline@next`. `npx proofline --version` prints the version you have. ## How you use it, start to finish @@ -126,6 +135,17 @@ Exit codes: `0` PASS, `1` FAIL, `2` BLOCKED, `3` usage. Every command takes `--j That is the short path: four commands, in that order. The diagram and the table below are the manual path. +`proofline setup` needs two things from your shell, and stops before it writes anything when either is missing, naming the command that provides it: + +- `AGENTKEY_HOME`: the directory that holds your owner key. Proofline never picks one for you. For example `export AGENTKEY_HOME="$HOME/.proofline-owner"`, in your shell profile, because `proofline contract sign` reads it too. +- `agentkey` on PATH: the owner key is made with [agentkey](https://github.com/mgillr/agentkey). `npm install --save-dev @optitransfer/agentkey` and run Proofline through `npx`, or `npm install -g @optitransfer/agentkey`. + +```bash +export AGENTKEY_HOME="$HOME/.proofline-owner" +npm install --save-dev @optitransfer/proofline@next @optitransfer/agentkey +npx proofline setup +``` + Once `guard/owner.pub` is pinned, `proofline contract sign` writes `proofline.json.sig` and `proofline contract verify-sig` checks it. `proofline contract propose` writes `proposals/.diff` and prints the hash. An unsigned or altered contract is refused with `contract not owner-signed`. Three parties: **you** (the owner of the repository), **your agent** (Claude Code, Cursor, a CI job), and **Proofline** (the hooks and the gate in your repository). The agent builds; Proofline checks; you decide twice, once on the rules and once on the result. diff --git a/docs/logo-128.png b/docs/logo-128.png new file mode 100644 index 0000000000000000000000000000000000000000..3597e1646aed6ed87a6752dbc33f2123e5aa08c1 GIT binary patch literal 2573 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�v9zGBwYyUNUxUicU&FeGm_o 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 literal 0 HcmV?d00001 diff --git a/docs/logo-white.svg b/docs/logo-white.svg new file mode 100644 index 0000000..ab1d54f --- /dev/null +++ b/docs/logo-white.svg @@ -0,0 +1 @@ + diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100644 index 0000000..4e0eb59 --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1 @@ +