Skip to content
Draft
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
120 changes: 120 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ npx proofloop productivity --write --baseline-source benchmark # verified produc
npx proofloop prompt # kickoff prompt to paste into your coding agent
npx proofloop this-repo --goal "proofloop my latest updates" --write-runner-plan
npx proofloop runner run --plan proofloop.runner.json --budget-usd 100
npx proofloop program run --plan proofloop.program.json --budget-usd 25
npx proofloop gate # run checks -> .proofloop/gate-state.json
```

Expand Down Expand Up @@ -294,6 +295,98 @@ you want the CLI to execute the plan with append-only state, budget control, and
npx proofloop this-repo --goal "proofloop my latest updates" --write-runner-plan --run --budget-usd 100
```

## Durable Program Supervisor (P0)

`proofloop program` coordinates a small dependency-safe program by invoking the existing durable
runner once per arc. It is intentionally not a second shell runner. Each arc points to an immutable
`proofloop-runner-plan-v1` subplan, runs sequentially after its dependencies pass, and may require a
locally verified ProofLoop receipt.

P0 admits only `read_only` and `proposal_only` arcs. Authority is a separate JSON file whose
canonical digest is pinned into durable program state. Any authority, program, or referenced runner
plan change blocks or fails the existing run rather than silently continuing. Explicit external
egress is rejected. Failed arcs are not automatically requeued; only an interrupted `running` arc
may recover through the existing runner's explicit stale-lock recovery path.

```json
// authority.json
{
"schema": "proofloop-program-authority-v1",
"authorityId": "overnight-read-propose-only",
"allowedArcModes": ["read_only", "proposal_only"],
"allowExternalEgress": false,
"maxBudgetUsd": 25,
"maxAttemptsPerArc": 1
}
```

```json
// proofloop.program.json
{
"schema": "proofloop-program-plan-v1",
"programId": "nodekit-ultra-v1",
"authorityPath": "authority.json",
"arcs": [
{
"id": "baseline",
"mode": "read_only",
"runnerPlan": "plans/baseline.runner.json"
},
{
"id": "proposal",
"mode": "proposal_only",
"runnerPlan": "plans/proposal.runner.json",
"dependsOn": ["baseline"],
"receipt": { "kind": "proofloop-envelope", "file": "proof/proposal-receipt.json" }
}
]
}
```

```bash
npx proofloop program run --plan proofloop.program.json --budget-usd 25
npx proofloop program resume --run-id latest
npx proofloop program status --run-id latest --json
npx proofloop program report --run-id latest
```

This is a local P0 safety boundary, not an OS sandbox. Runner subplans still require an execution
environment that independently enforces network, credential, browser, deployment, and publish
authority.

### NodeKit compiled-proof binding

NodeKit-generated `proof/release-proof.json` is not sufficient by itself to certify an application:
it must also bind to the exact candidate commit and the compiler's current resolved definition.

```bash
npx proofloop program verify-nodekit \
--file proof/release-proof.json \
--candidate-commit "$(git rev-parse HEAD)" \
--minimum-level local-ready
```

The verifier stays local and read-only. It fails closed when the candidate commit, compiled
`configHash`, raw `nodeagent.yaml` manifest digest, discovered source-file bytes, deterministic
demo/evaluation receipts, or required release receipts disagree. `--minimum-level release-ready`
also requires the live, browser, and deployment receipts NodeKit declares as release gates.

An arc can use the same binding rather than a generic receipt:

```json
{
"kind": "nodekit-proof",
"file": "proof/release-proof.json",
"candidateCommit": "<40-or-64-char-lowercase-git-sha>",
"minimumLevel": "local-ready"
}
```

The binding validates the files NodeKit's compiler discovered. It deliberately does not claim to
cover source files omitted from that discovery contract; widening compiler discovery remains a
NodeKit compiler responsibility. It also verifies already-produced local receipts only: it never
deploys, invokes a provider, publishes, or promotes a candidate.

## How The Stop Gate Decides

- Default check-only mode reads `.proofloop/gate-state.json` with no subprocess or network call.
Expand Down Expand Up @@ -358,13 +451,19 @@ script. With neither, it reports `no_gate` with exit code 2. An unconfigured gat
| `proofloop report latest [--json]` | Summarize the latest gate receipt. |
| `proofloop charts latest` | Write local JSON/SVG proof charts under `.proofloop/charts/`. |
| `proofloop receipt verify --file <path>` | Verify app-produced proof receipts such as NodeAgent ingestion receipts. |
| `proofloop receipt envelope verify --file <path>` | Verify a `proofloop.receipt/v1` envelope, authority semantics, and local content hashes. |
| `proofloop receipt schema [--json]` | Locate or print the packaged `proofloop.receipt/v1` JSON Schema. |
| `proofloop solo setup --source <repo> --agent both` | Install one canonical Solo skill for Codex and Claude Code and compose one Stop gate. |
| `proofloop solo ingest --file <envelope> --write-runner-plan` | Validate Solo evidence and optionally compile advisory tasks without executing them. |
| `proofloop solo status\|resume\|gate` | Inspect or enforce the NodeProof-derived Solo interop state. |
| `proofloop runner run --plan <file> --budget-usd 100` | Run an append-only, budgeted task plan under `.proofloop/runner/runs/<runId>/`. |
| `proofloop runner resume --run-id latest --clear-stale-lock` | Resume a runner after a crash; stale `running` tasks are requeued after explicit stale-lock clearance. |
| `proofloop runner status --run-id latest [--json]` | Inspect durable runner state and ledger paths. |
| `proofloop runner report --run-id latest [--json]` | Print the runner honesty report with per-family/per-model pass rate and estimated cost/pass. |
| `proofloop program run --plan <file> --budget-usd <n>` | Run a P0 read/proposal-only program as dependency-safe runner subplans under `.proofloop/programs/runs/<runId>/`. |
| `proofloop program resume --run-id latest` | Resume queued arcs, or explicitly recover an interrupted running arc through the runner. Authority or referenced-plan changes fail closed; failed arcs are not requeued. |
| `proofloop program status\|report --run-id latest [--json]` | Inspect the pinned authority digest, program state, arc statuses, budget, and ledger. |
| `proofloop program verify-nodekit --file <proof/release-proof.json> --candidate-commit <sha>` | Locally bind a NodeKit proof receipt to the exact checked-out candidate and compiler discovery; no deploy or external action occurs. |
| `proofloop mcp` | Start the optional read-only MCP server. |
| `proofloop gate [--check]` | Run configured checks or `npm test`; exit 0 pass, 1 fail, 2 unusable. |
| `proofloop hooks install\|uninstall\|status` | Install/remove/status Claude Code Stop, PreToolUse, and PostToolUse hooks. |
Expand Down Expand Up @@ -421,6 +520,27 @@ The verifier checks the receipt type/version, `ok: true`, document-pool to memor
created document and memory-object counts, proof hashes/keys, zero source/chunk failures, and positive
batch/concurrency config. Failed receipts exit 1, while malformed CLI usage exits 2.

### Canonical receipt envelope

`proofloop.receipt/v1` is the general transport envelope for gate, Solo, hosted, UI-QA, evaluation,
runner, maturity, and app-specific receipts. It preserves each existing payload under a versioned,
content-hashed `payload` field while keeping the verdict authority separate:

- Only deterministic gates or official scorers may produce an authoritative verdict.
- Model judges, human reviews, and imported pass claims remain advisory.
- Decisive checks must reference locally verifiable, content-hashed evidence.
- Inline payloads use sorted-key canonical JSON SHA-256; referenced files use raw-byte SHA-256.

```bash
npx proofloop receipt schema
npx proofloop receipt schema --json
npx proofloop receipt envelope verify --file proof/receipt.json
```

See [`docs/receipt-envelope-v1.md`](docs/receipt-envelope-v1.md) for the public TypeScript API,
authority rules, and migration mapping for existing schemas. Existing receipt schemas and the
`receipt verify --kind nodeagent-ingestion` command remain supported.

## Scope

This package is the portable core: gate, refuse-fake-done hooks, expected-tool-use contracts,
Expand Down
101 changes: 97 additions & 4 deletions dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@ const proofloopHooks_1 = require("./proofloopHooks");
const proofloopCi_1 = require("./proofloopCi");
const proofloopToolUse_1 = require("./proofloopToolUse");
const receipts_1 = require("./receipts");
const proofReceipt_1 = require("./proofReceipt");
const mcp_1 = require("./mcp");
const project_1 = require("./project");
const runner_1 = require("./runner");
const program_1 = require("./program");
const nodekitProof_1 = require("./nodekitProof");
const easeProof_1 = require("./easeProof");
const targetPlan_1 = require("./targetPlan");
const hosted_1 = require("./hosted");
const maturity_1 = require("./maturity");
Expand Down Expand Up @@ -113,11 +117,15 @@ function usage() {
" report latest [--json] latest gate report",
" charts latest write local JSON/SVG proof charts",
" receipt verify --file <path> verify app-produced proof receipts",
" receipt envelope verify --file <path> verify a proofloop.receipt/v1 envelope",
" receipt schema [--json] locate or print the proofloop.receipt/v1 JSON Schema",
" ease verify --manifest <path> [--out <receipt>] verify NodeKit EaseProof evidence integrity without inventing usability authority",
" solo setup --source <path> [--agent codex|claude-code|both] [--install-deps] [--verify]",
" solo ingest|status|gate|resume validate and inspect Solo interop evidence",
" solo attest --file <envelope> --gate-receipt <receipt> --out <receipt> --key-id <id>",
" solo verify-attestation --file <receipt> [--public-key-file <pem>] [--key-id <id>]",
" runner run|resume|status|report durable append-only task runner with budget and resume",
" program run|resume|status|report|verify-nodekit P0 program supervisor and local NodeKit proof binding",
" hosted intake|validate|dashboard|run create or resume a hosted URL proof packet",
" target [--url <url>] [--write-runner-plan] [--write-browser-smoke] recommend benchmark families and write target/context receipts",
" maturity [--dense|--json|--write] [--target-level 5] judge agent-era codebase/app maturity and missing layers",
Expand Down Expand Up @@ -190,11 +198,15 @@ function runCli(argv) {
case "charts":
return runChartsCommand(positional[1], root);
case "receipt":
return runReceiptCommand(positional[1], options, root);
return runReceiptCommand(positional[1], positional[2], options, root);
case "ease":
return runEaseCommand(positional[1], options, root);
case "solo":
return runSoloCommand(positional[1], options, root);
case "runner":
return runRunnerCommand(positional[1], options, root);
case "program":
return runProgramCommand(positional[1], options, root);
case "hosted":
return runHostedCommand(positional[1], options, root);
case "target":
Expand Down Expand Up @@ -743,9 +755,36 @@ function runChartsCommand(sub, root) {
console.log(`proofloop charts: wrote ${result.svgPath}`);
return 0;
}
function runReceiptCommand(sub, options, root) {
if (sub !== "verify") {
console.error("proofloop receipt: expected `verify`.");
function runReceiptCommand(sub, action, options, root) {
if (sub === "schema") {
if (action !== undefined) {
console.error("proofloop receipt schema: unexpected positional argument.");
return 2;
}
if (options.json === true)
console.log(JSON.stringify((0, proofReceipt_1.readProofReceiptSchema)(), null, 2));
else
console.log((0, proofReceipt_1.proofReceiptSchemaPath)());
return 0;
}
if (sub === "envelope") {
if (action !== "verify") {
console.error("proofloop receipt envelope: expected `verify`.");
return 2;
}
const filePath = str(options.file);
if (!filePath) {
console.error("proofloop receipt envelope verify: --file <path> is required.");
return 2;
}
return (0, proofReceipt_1.runProofReceiptEnvelopeVerify)({
root,
filePath,
json: options.json === true,
});
}
if (sub !== "verify" || action !== undefined) {
console.error("proofloop receipt: expected `verify`, `envelope verify`, or `schema`.");
return 2;
}
const filePath = str(options.file);
Expand All @@ -767,6 +806,19 @@ function runReceiptCommand(sub, options, root) {
json: options.json === true,
});
}
function runEaseCommand(sub, options, root) {
if (sub !== "verify") {
console.error("proofloop ease: expected `verify`.");
return 2;
}
const manifestPath = str(options.manifest) ?? "proof/ease/latest/manifest.json";
return (0, easeProof_1.runEaseProofVerify)({
root,
manifestPath,
...(str(options.out) !== undefined ? { outputPath: str(options.out) } : {}),
json: options.json === true,
});
}
async function runRunnerCommand(sub, options, root) {
if (sub !== "run" && sub !== "resume" && sub !== "status" && sub !== "report") {
console.error("proofloop runner: expected `run`, `resume`, `status`, or `report`.");
Expand All @@ -786,6 +838,47 @@ async function runRunnerCommand(sub, options, root) {
});
return result.exitCode;
}
async function runProgramCommand(sub, options, root) {
if (sub === "verify-nodekit") {
const releaseProofPath = str(options.file);
const candidateCommit = str(options["candidate-commit"]);
if (!releaseProofPath || !candidateCommit) {
console.error("proofloop program verify-nodekit: requires --file <proof/release-proof.json> and --candidate-commit <sha>.");
return 2;
}
const minimumLevel = str(options["minimum-level"]);
if (minimumLevel !== undefined && minimumLevel !== "local-ready" && minimumLevel !== "release-ready") {
console.error("proofloop program verify-nodekit: --minimum-level must be local-ready or release-ready.");
return 2;
}
return (0, nodekitProof_1.runNodekitProofBindingVerify)({
root,
releaseProofPath,
candidateCommit,
...(minimumLevel !== undefined ? { minimumLevel: minimumLevel } : {}),
...(str(options["compiled-definition"]) !== undefined ? { compiledDefinitionPath: str(options["compiled-definition"]) } : {}),
...(str(options["config-hash-file"]) !== undefined ? { configHashPath: str(options["config-hash-file"]) } : {}),
...(str(options.discovery) !== undefined ? { discoveryPath: str(options.discovery) } : {}),
json: options.json === true,
});
}
if (sub !== "run" && sub !== "resume" && sub !== "status" && sub !== "report") {
console.error("proofloop program: expected `run`, `resume`, `status`, `report`, or `verify-nodekit`.");
return 2;
}
const result = await (0, program_1.runProofloopProgram)({
root,
subcommand: sub,
...(str(options.plan) !== undefined ? { planPath: str(options.plan) } : {}),
...(str(options["run-id"]) !== undefined ? { runId: str(options["run-id"]) } : {}),
...(num(options["budget-usd"]) !== undefined ? { budgetUsd: num(options["budget-usd"]) } : {}),
...(num(options["max-arcs"]) !== undefined ? { maxArcs: num(options["max-arcs"]) } : {}),
...(num(options["lock-ttl-ms"]) !== undefined ? { lockTtlMs: num(options["lock-ttl-ms"]) } : {}),
clearStaleLock: options["clear-stale-lock"] === true,
json: options.json === true,
});
return result.exitCode;
}
async function runTargetCommand(options, root) {
const result = await (0, targetPlan_1.runProofloopTarget)({
root,
Expand Down
24 changes: 24 additions & 0 deletions dist/easeProof.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type ProofReceiptEnvelope } from "./proofReceipt";
export interface EaseProofVerification {
ok: boolean;
easeCertified: boolean;
errors: string[];
warnings: string[];
manifestPath: string;
browserManifestPath?: string;
checkedScreenshots: number;
checkedReplayArtifacts: number;
envelope?: ProofReceiptEnvelope;
outputPath?: string;
}
export declare function verifyEaseProof(options: {
root: string;
manifestPath: string;
outputPath?: string;
}): EaseProofVerification;
export declare function runEaseProofVerify(options: {
root: string;
manifestPath: string;
outputPath?: string;
json?: boolean;
}): number;
Loading
Loading