Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .proofline/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"approvals": ".proofline/approvals"
}
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

All notable project changes are recorded here. Dates use ISO 8601 (`YYYY-MM-DD`).

## Unreleased
## 0.2.0-next.2 (2026-10-21)

### 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).
- The tests column of a claim line takes `crit:<criterion id>` for a task whose proof is a command the gate runs rather than a test file it adds, such as an intake or measurement task. The name holds only when it is a criterion of that task, it ran in this gate, its type is `command`, `metric`, `test_red_green` or `docs`, and it passed. An unknown id, another task's criterion, a `grep`, `file_exists`, `scope` or `wiring` check, or a criterion that did not pass is a claim mismatch that names the reason. Other names are still looked up in the repository, and `-`, `n/a` and `none` still name no test.

### 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.
- A `grep` pattern that starts with `(?i)`, `(?s)` or `(?m)`, the way a Python verifier writes flags, runs with those flags. It crashed as an invalid group, so a contract shared with a Python gate was BLOCKED here and PASS there.

## 0.2.0-next.1 (2026-10-17)

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@optitransfer/proofline",
"version": "0.2.0-next.1",
"version": "0.2.0-next.2",
"description": "Independent V&V for coding agents. The agent says done; Proofline re-runs every check against a contract, rejects claims the evidence does not support, and gates the push.",
"keywords": [
"ai-agents",
Expand Down
198 changes: 197 additions & 1 deletion proofline.json
Original file line number Diff line number Diff line change
Expand Up @@ -3832,9 +3832,205 @@
"If a criterion cannot pass without loosening a threshold, skipping or deleting a test, raising a timeout, widening the scope or editing the contract: STOP and report file:line and the measured value.",
"If the environment a criterion needs is missing (repository admin, npm owner login, Actions, a second machine): report BLOCKED naming the missing item; never mark it passed by hand."
]
},
{
"id": "PL-CRIT01",
"title": "A claim can name an executed criterion with crit:<id>, and a grep pattern may start with (?i) or (?s)",
"phase": "implement",
"owner": "lane-a",
"depends_on": [],
"requirements": [
"REQ-008",
"REQ-009"
],
"scope": {
"allow": [
"src/claims.mjs",
"src/engine.mjs",
"src/checks.mjs",
"test/claims_crit.test.mjs",
"test/grep_inline_flags.test.mjs",
"CHANGELOG.md"
]
},
"description": "Two gaps found on 2026-10-20. (1) checkClaim (src/claims.mjs:40-46) accepts a tests-column name only when git grep -F or git ls-files finds it, and drops '-', 'n/a' and 'none', so an intake or measurement task, whose proof is a command the gate runs and not a test file it adds, cannot make an honest done claim (the oll4com/spaceapp #64 intake task PR64-00 had to be recorded PASS-BY-EVIDENCE). The tests column takes crit:<criterion id>; it holds only when the id is a criterion of the claimed task that ran in this gate, of type command, metric, test_red_green or docs, with status pass. An unknown id, another task's criterion, a grep, file_exists, scope or wiring check, or a criterion that did not pass is a claim mismatch naming the reason. The gate passes checkClaim the id, type and status of each criterion it ran (src/engine.mjs). Any other name is still looked up in the repository, and '-', 'n/a' and 'none' still name no test. (2) the grep verifier (src/checks.mjs:211-214) builds new RegExp(v.pattern, v.flags || 'm'); a leading (?i) or (?s), as a Python verifier writes flags, throws 'Invalid group', so the x402-conformance contract (XC-00-AC03, XC-04-AC12, XC-04-AC13, XC-M2-AC02) is BLOCKED here and PASS in its own CI. A leading (?i), (?s) or (?m) becomes flags added to the criterion's own; a pattern without one is unchanged and stays case-sensitive. Both are recorded under Unreleased in CHANGELOG.md.",
"criteria": [
{
"id": "PL-CRIT01-AC01",
"mandatory": true,
"statement": "crit:<id> holds for a passing executed criterion of the task and is refused, with the reason, for an unknown id, another task, a check that runs nothing and a failing criterion",
"verifier": {
"type": "test_red_green",
"test_file": "test/claims_crit.test.mjs",
"cmd": "node --test {test}"
}
},
{
"id": "PL-CRIT01-AC02",
"mandatory": true,
"statement": "a grep pattern starting with (?i) or (?s) runs with those flags instead of crashing, and a pattern without one stays case-sensitive",
"verifier": {
"type": "test_red_green",
"test_file": "test/grep_inline_flags.test.mjs",
"cmd": "node --test {test}"
}
},
{
"id": "PL-CRIT01-AC03",
"mandatory": true,
"statement": "Full suite exits 0, nothing hung",
"verifier": {
"type": "command",
"cmd": "npm test",
"expect_exit": 0,
"timeout_s": 1800
}
},
{
"id": "PL-CRIT01-AC04",
"mandatory": true,
"statement": "the changelog records the crit:<id> claims form under Unreleased",
"verifier": {
"type": "grep",
"file": "CHANGELOG.md",
"pattern": "^- The tests column of a claim line takes `crit:<criterion id>`",
"must": "present",
"flags": "m"
}
},
{
"id": "PL-CRIT01-AC05",
"mandatory": true,
"statement": "the changelog records the inline-flag grep fix",
"verifier": {
"type": "grep",
"file": "CHANGELOG.md",
"pattern": "^- A `grep` pattern that starts with `\\(\\?i\\)`",
"must": "present",
"flags": "m"
}
},
{
"id": "PL-CRIT01-AC06",
"mandatory": true,
"statement": "crit: names are resolved on the default gate path",
"verifier": {
"type": "wiring",
"entry": "bin/proofline.mjs",
"symbol": "critIssues"
}
},
{
"id": "PL-CRIT01-AC07",
"mandatory": true,
"statement": "Changes stay in scope",
"verifier": {
"type": "scope"
}
},
{
"id": "PL-CRIT01-AC08",
"mandatory": true,
"statement": "Committed and pushed to main",
"verifier": {
"type": "pushed"
}
}
],
"stop_rules": [
"If a criterion cannot pass without loosening a threshold, skipping or deleting a test, raising a timeout, widening the scope or editing the contract: STOP and report file:line and the measured value.",
"If the environment a criterion needs is missing (repository admin, npm owner login, Actions, a second machine): report BLOCKED naming the missing item; never mark it passed by hand."
]
},
{
"id": "PL-NEXT2",
"title": "Version 0.2.0-next.2, so npm carries PL-CRIT01",
"phase": "release",
"owner": "lane-a",
"depends_on": [
"PL-CRIT01"
],
"requirements": [
"PL-R01",
"TB-R07"
],
"scope": {
"allow": [
"package.json",
"package-lock.json",
"CHANGELOG.md"
]
},
"description": "npm's @optitransfer/proofline is at 0.2.0-next.1 (latest and next, measured 2026-10-20), which predates PL-CRIT01. The version moves to 0.2.0-next.2 in package.json and package-lock.json (npm install --package-lock-only --ignore-scripts, empty user config), and the Unreleased heading in CHANGELOG.md becomes \"## 0.2.0-next.2 (2026-10-20)\". The owner publishes it from his terminal from the landed commit (npm publish --access public --tag next --auth-type=web) and records the registry's version and bin.",
"criteria": [
{
"id": "PL-NEXT2-AC01",
"mandatory": true,
"statement": "package.json says 0.2.0-next.2",
"verifier": {
"type": "grep",
"file": "package.json",
"pattern": "\"version\":\\s*\"0\\.2\\.0-next\\.2\"",
"must": "present"
}
},
{
"id": "PL-NEXT2-AC02",
"mandatory": true,
"statement": "the changelog dates 0.2.0-next.2 and keeps no Unreleased heading",
"verifier": {
"type": "command",
"cmd": "node -e \"const s=require('fs').readFileSync('CHANGELOG.md','utf8');process.exit(/^## 0\\.2\\.0-next\\.2 \\(2026-10-[0-9]{2}\\)$/m.test(s)&&!/^## Unreleased$/m.test(s)?0:1)\"",
"expect_exit": 0,
"timeout_s": 60
}
},
{
"id": "PL-NEXT2-AC03",
"mandatory": true,
"statement": "the packed tarball is 0.2.0-next.2, holds bin/proofline.mjs and the manifest names it as the proofline command",
"verifier": {
"type": "command",
"cmd": "sh -c \"npm pack --dry-run --json --ignore-scripts > /tmp/pl-pack.json && node -e \\\"const p=require('/tmp/pl-pack.json')[0];process.exit(p.version==='0.2.0-next.2'&&p.files.some(f=>f.path==='bin/proofline.mjs')&&require('./package.json').bin.proofline==='bin/proofline.mjs'?0:1)\\\"\"",
"expect_exit": 0,
"timeout_s": 300
}
},
{
"id": "PL-NEXT2-AC04",
"mandatory": true,
"statement": "Full suite exits 0, nothing hung",
"verifier": {
"type": "command",
"cmd": "npm test",
"expect_exit": 0,
"timeout_s": 1800
}
},
{
"id": "PL-NEXT2-AC05",
"mandatory": true,
"statement": "Changes stay in scope",
"verifier": {
"type": "scope"
}
},
{
"id": "PL-NEXT2-AC06",
"mandatory": true,
"statement": "Committed and pushed to main",
"verifier": {
"type": "pushed"
}
}
],
"stop_rules": [
"If a criterion cannot pass without loosening a threshold, skipping or deleting a test, raising a timeout, widening the scope or editing the contract: STOP and report file:line and the measured value.",
"If the environment a criterion needs is missing (repository admin, npm owner login, Actions, a second machine): report BLOCKED naming the missing item; never mark it passed by hand."
]
}
],
"notes": "Each task carries the requirement ids it covers; next --owner hands out the tasks of one owner. Cross-repository needs are written into task text as an external dependency and reported BLOCKED when absent. PL-SELF stays: proofline verifies itself with its own contract. | 2026-10-17: 15 review tasks appended (COVERAGE.md maps every finding and open issue). Scope criterion added to PL-SELF, DG-PL so the contract passes its own validator. | 2026-10-17: PL-SETUP01 (setup pins the owner key) and PL-NEXT1 (0.2.0-next.1 carries the bin) appended.",
"notes": "Each task carries the requirement ids it covers; next --owner hands out the tasks of one owner. Cross-repository needs are written into task text as an external dependency and reported BLOCKED when absent. PL-SELF stays: proofline verifies itself with its own contract. | 2026-10-17: 15 review tasks appended (COVERAGE.md maps every finding and open issue). Scope criterion added to PL-SELF, DG-PL so the contract passes its own validator. | 2026-10-17: PL-SETUP01 (setup pins the owner key) and PL-NEXT1 (0.2.0-next.1 carries the bin) appended. | 2026-10-20: PL-CRIT01 (crit:<id> claims for tasks proved by a command; leading (?i)/(?s) grep flags) and PL-NEXT2 (0.2.0-next.2) appended.",
"ci": {
"tasks": [
"PL-SELF"
Expand Down
1 change: 1 addition & 0 deletions proofline.json.sig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"payloadType":"application/vnd.proofline.contract-sha256","payload":"OWQzM2VjOWJlZWUxYmQyMmViNmJjZjk5NzVhZmRkN2UyZGJkZTc0MmVjMDQwYjE4NzdkMTE3YzMyMTBlZDMzNA==","signatures":[{"keyid":"kb71a7ce9e7b76859","sig":"ckpux3El8CYsS74U7YeISFDbO7cKboLT8U/Luj/4XXoKUXNLV3h0AOSTwPMKMi/B0HfuL4EGv2e61LD2AnGADA=="}]}
4 changes: 3 additions & 1 deletion src/checks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ export const verifiers = {
const f = path.join(ctx.root, v.file);
if (!fs.existsSync(f)) return { status: 'fail', detail: `${v.file} not found` };
const txt = fs.readFileSync(f, 'utf8');
const re = new RegExp(v.pattern, v.flags || 'm');
// A leading (?i), (?s) or (?m) is how Python writes these flags; JavaScript refuses it as an invalid group.
const lead = String(v.pattern).match(/^\(\?([ims]+)\)/);
const re = new RegExp(lead ? v.pattern.slice(lead[0].length) : v.pattern, [...new Set((v.flags || 'm') + (lead ? lead[1] : ''))].join(''));
const m = txt.match(re);
const want = (v.must || 'present') === 'present';
if (!!m === want) return { status: 'pass', detail: `/${v.pattern}/ ${want ? 'present' : 'absent'} in ${v.file}`, evidence: m ? { line: txt.slice(0, m.index).split('\n').length, text: m[0] } : undefined };
Expand Down
18 changes: 18 additions & 0 deletions src/claims.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ import { git, onRemote } from './util.mjs';
const DONE = /^(done|complete(d)?|verified|pass(ed)?|fixed|finished|green)$/i;
const HEDGE = /\b(should work|likely|probably|untested|not tested|later|follow[- ]?up|out of scope|defer(red)?|todo|next session|in a future)\b/i;

// `crit:<criterion id>` in the tests column names one of the task's own executed criteria instead of a file in the
// repo, for a task whose proof is a command run by the gate and not a test it adds (an intake or measurement task).
const CRIT = /^crit:(\S+)$/i;
const EXECUTED = ['command', 'metric', 'test_red_green', 'docs'];

/** Why `crit:<id>` does not prove the claim, or nothing: it must be a criterion of this task that ran a command in this gate and passed. */
export function critIssues(id, task, res) {
const rows = res?.criteria;
if (!Array.isArray(rows)) return [`named criterion "crit:${id}" cannot be checked: the gate passed no criterion results`];
const r = rows.find((x) => x.id === id);
if (!r) return [`named criterion "crit:${id}" is not a criterion of ${task} that this gate ran`];
if (!EXECUTED.includes(r.type)) return [`named criterion "crit:${id}" is a ${r.type} check, which runs no test: name a command, metric, test_red_green or docs criterion`];
if (r.status !== 'pass') return [`named criterion "crit:${id}" is ${String(r.status).toUpperCase()} in this gate, not PASS`];
return [];
}

/** Lines: TASK-ID | status | commit | tests | measured vs target | missing */
export function parseClaims(text, taskIds) {
const out = [];
Expand Down Expand Up @@ -42,6 +58,8 @@ export function checkClaim(root, claim, res, { requirePushed = false, remote = '
if (claimedDone && !names.length) issues.push('claimed done without naming the test(s) that prove it');
for (const n of names) {
const base = n.replace(/\s*\(.*\)$/, '').split(/\s+/)[0];
const crit = base.match(CRIT);
if (crit) { issues.push(...critIssues(crit[1], claim.id, res)); continue; }
if (!git(['grep', '-q', '-F', base], root).ok && !git(['ls-files', '--', `*${base}*`], root).out) issues.push(`named test "${base}" not found in the repo`);
}
const keys = Object.keys(res.measured || {});
Expand Down
2 changes: 1 addition & 1 deletion src/engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export function verifyTask(root, taskId, opts = {}) {
for (const a of t.criteria) if (a.verifier.type === 'metric') metricLabels[a.verifier.name || a.verifier.key] = a.verifier.labels || [];
const claimResults = [];
if (claims) {
const res = { decision, failing: [...blocked, ...failing], measured: ctx.measured, metricLabels };
const res = { decision, failing: [...blocked, ...failing], measured: ctx.measured, metricLabels, criteria: results.map(({ id, type, status }) => ({ id, type, status })) };
const needPush = t.criteria.some((a) => a.verifier.type === 'pushed');
for (const cl of parseClaims(claims, [t.id])) claimResults.push(checkClaim(root, cl, res, { requirePushed: needPush, ...pushTarget(c, t) }));
for (const cr of claimResults) if (!cr.ok) failing.push(...cr.issues.map((i) => `claim mismatch: ${i}`));
Expand Down
Loading
Loading