graftfork is now in Claude's connector directory. Connect it in Claude

graftfork

Replay of real changes

Green at 78 °F. Changed in the record, not the code.

heat-engine turns a wet-bulb temperature into a heat flag. We wanted its green flag to start at 78 °F instead of 80 °F. Nobody opened the implementation. We edited the program's duramen record, Graft checked the edit for free, rebuilt the program blind, and showed every answer that changed. This is that change, step by step, from the stored runs. The three side by side come first; after the 78 °F replay come a label rename and a new rule, two more real changes to the same record.

  1. Before
  2. The edit
  3. Free check
  4. Rebuild
  5. After
  6. Receipt
  7. The honest part

Three changes, side by side

Each rebuild changed what its edit asked for, and nothing else

Three real changes to heat-engine's duramen record in the public graft-lab repository. Each was rebuilt blind from the edited record, and sent the same requests as a rebuild of the record before the edit. Every number below is from the stored runs and their signed records.

Each change was pushed to a branch of its own and rebuilt as a draft with Try a change in Specs. Change the record here does the same without a branch: Graft holds the edit (a change order).

The changeWhat the record asked forFree checkRebuildAnswers changedChanged without being asked
Green at 78 °Flab/heat-78The white band ends at 78 °F, not 80 °F (FL-001), plus two lines of oracle.Strong, after a record-only edit graded thin with 6 errorsPassed, 215 of 215 cases, $0.8116 flags of 1,892 requests; 6 paths of the answer, all explained0
A label renamelab/heat-renameFL-003's title gains one word. Prose only: duramen diff calls it a patch.None stored for this change; the rebuild's own check of the record found 0 errorsPassed, 215 of 215 cases, $0.860 of 1,760 requests0
A new rulelab/heat-unitsflagF takes an optional units; anything but "F" is a bad_request (new FL-007), plus one line of oracle.StrongPassed, 217 of 217 cases, $0.89194 of 1,960 requests, each now bad_request; 15 paths, all explained0

A prose edit changed no answer at all. A rule change changed only answers its requirement names: each of the 194 is a request that sent a units other than "F". The flag edge moved 16 flags and the audit limits that go with them. In all three, the count of changes nobody asked for is 0.

Requests per diff differ because the probes come from each record: its examples, its breaking points and a sweep. The 78 °F change is compared with base rebuild q-4zFdlqCU-lLzTaaEy2fg; the other two with DgHyeHH8U--OK9lwfiAU_w, a second rebuild of the same unchanged record (d843e00, 215 of 215 cases, $0.31) whose own diff against the first found 0 changed paths in 1,628 requests.

1. Before

At 79 °F, the program says white

The record's flag table (requirement FL-001) said white below 80 °F and green from 80 °F up to 85 °F. Graft's base rebuild of that record passed every case.

heat-engine at duramen record d843e00
flagF {"wetBulbF": 79} white flagDartLabel: "low"

From the record's FL-001 table at d843e00 (w < 80 is white, low). Base rebuild q-4zFdlqCU-lLzTaaEy2fg: heat-engine 1.5.0-slice, 215 of 215 cases and 1,500 hidden requests passed, $0.82.

2. The edit

One sentence of intent, twelve lines of record

The intent: green starts at 78 °F instead of 80 °F. Here is every line that changed, from git diff lab/heat..lab/heat-78 in the public graft-lab repository, grouped by what each part is for. Three files, 12 lines in, 12 out. Unchanged lines are trimmed.

The requirement table

heat.duramen, FL-001 "Flag from °F"

    | flag | wet-bulb °F | label |-   | `white` | `w < 80` | `low` |-   | `green` | `80 ≤ w < 85` | `moderate` |+   | `white` | `w < 78` | `low` |+   | `green` | `78 ≤ w < 85` | `moderate` |    | `yellow` | `85 ≤ w < 88` | `high` |  table flagF    | wetBulbF | result.flag | result.flagDartLabel |-   | 79.99    | "white"     | "low"                |-   | 80       | "green"     | "moderate"           |+   | 77.99    | "white"     | "low"                |+   | 78       | "green"     | "moderate"           |    | 84.99    | "green"     | "moderate"           |    | 85       | "yellow"    | "high"               |

The words, and the worked examples that pin the new edge on both sides.

The constants

heat.duramen, FL-002 "Flag audit record"

    `inputs` `{"wetBulbF": wetBulbF}`; `constants`-   `{"white_max":80,"green_max":85,"yellow_max":88,"red_max":90}`; `result_summary`+   `{"white_max":78,"green_max":85,"yellow_max":88,"red_max":90}`; `result_summary`  example flagF {"wetBulbF": 86.5}-   expect audit.constants = {"white_max": 80, "green_max": 85, "yellow_max": 88, "red_max": 90}+   expect audit.constants = {"white_max": 78, "green_max": 85, "yellow_max": 88, "red_max": 90}

Every flag answer carries an audit with the limits it used, so the audit's white_max changes too.

The boundary row

heat.duramen, FL-004 "Flag from °C" and FL-005

  table flagC    | wetBulbC           | result.flag |-   | 26.66666666666666  | "white"     |+   | 26.66666666666666  | "green"     |    | 30                 | "yellow"    |  example flagC {"wetBulbC": 26.66666666666666}-   expect audit.result_summary = "wetBulbC=26.66666666666666 → wetBulbF=80.0000 → white"+   expect audit.result_summary = "wetBulbC=26.66666666666666 → wetBulbF=80.0000 → green"

26.66666666666666 °C converts to a hair under 80 °F (it prints as 80.0000). That was white under the old edge and is green under the new one.

The evidence

evidence/flag-mapping.fixtures.csv, notes trimmed

id,source_ref,wet_bulb_f,expected_flag,expected_flag_dart_label,notes-fm-003-just-below-white-green,usmc-6200-1e,79.99,white,low,…-fm-004-at-white-green,usmc-6200-1e,80.00,green,moderate,… (≥ 80 is green).+fm-003-just-below-white-green,usmc-6200-1e,77.99,white,low,…+fm-004-at-white-green,usmc-6200-1e,78.00,green,moderate,… (≥ 78 is green).fm-005-just-above-white-green,usmc-6200-1e,80.01,green,moderate,…

The table of outside evidence the record is checked against moves its two edge rows.

The oracle a person approves this

the model the judge uses: oracle.mjs

const FLAG_CITE = 'USMC 6200.1E Table 3-1';-const BOUNDS = { white_max: 80, green_max: 85, yellow_max: 88, red_max: 90 };+const BOUNDS = { white_max: 78, green_max: 85, yellow_max: 88, red_max: 90 };const LABEL = { white: 'low', green: 'moderate', yellow: 'high', red: 'extreme', black: 'critical' };-const band = (w) => (w < 80 ? 'white' : w < 85 ? 'green' : w < 88 ? 'yellow' : w < 90 ? 'red' : 'black');+const band = (w) => (w < 78 ? 'white' : w < 85 ? 'green' : w < 88 ? 'yellow' : w < 90 ? 'red' : 'black');

The oracle is the program written as a small model. The judge asks it for the right answer to every hidden request, so it has to say the same thing the record says. Two lines of code, read and approved by a person.

3. The free check

A half-done edit is caught before anything is spent

First we checked an edit that changed the record but not the oracle. Graft ran duramen check and graded it thin: the examples now said green, the oracle still said white. Then the full edit, oracle included, graded strong. Neither check runs a model, and neither cost anything.

The edit is breaking (a reading of 79 °F was white and is now green), and the record still says 1.5.0-slice. A draft rebuild isn't held to the version; a change order is, and would wait for 2.0.0-slice (Change it in Graft).

Two draft checks, no model, no spend
lab/heat-78-record-only
record edited, oracle not
Thin

The record has 6 errors in duramen check, so nothing can be built from it yet.

  1. FL-001 at 78 °F: flag should be green, the oracle gives white
  2. FL-001 at 78 °F: label should be moderate, the oracle gives low
  3. FL-002: white_max should be 78, the oracle gives 80
  4. FL-004 at 26.66666666666666 °C should be green, the oracle gives white
  5. FL-005 summary should end green, the oracle gives white
  6. evidence fm-004 at 78.00 °F: green, the oracle gives white
Stopped before any build. Cost: $0

Run nO-27YxkgGO7GEx_2h-sOA, commit d89c7fa. The run stores the grade and the count. The six lines summarise the errors duramen check (0.3.0, run locally) prints for that commit; the run itself used duramen 0.2.0.

lab/heat-78
record and oracle edited
Strong

Every requirement has its own examples, and the cases catch 100% of the mistakes planted in the oracle that change an answer.

0 errors22 requirements69 examples80 evidence rows117 planted mistakes caught, 0 missed
Ready to rebuild. Cost: $0

Run Hnyk6Wfh8z2bNCbrOjsK1g, commit c6e9da9.

4. The rebuild

Rebuilt blind from the edited record

Graft wrote a brief from the record, two builders that never saw the code wrote the program again, and a judge they can't reach tested each build. The first build passed, and waits as a draft.

Draft rebuild of lab/heat-78

1. Brief

SPEC.md, DECISIONS.md and the build instructions, from the record at c6e9da9 with duramen v0.2.0.

2. Blind builds

build 1$0.39
build 2$0.42

Brief only: no repository, no token, no network.

3. Judge

the record's suite
215 of 215 pass
hidden requests vs the oracle
1,500 of 1,500 agree

4. Passed (a draft)

  • build 1, waits on graft/regen/po1qEN54ReZWLwQmIakuqQ
  • to be taken, never a pull request, never the spec's kept build
215/215cases, both builds
1,500hidden requests, 0 disagreements (seed 494272474)
$0.81for both builds; the base rebuild was $0.82
7 minfrom start to signed record

Run po1qEN54ReZWLwQmIakuqQ, started 02:37:39 UTC and finished 02:44:50 UTC on November 1, 2026. Compared with base rebuild q-4zFdlqCU-lLzTaaEy2fg.

5. After

What changed, and what didn't

Graft sent the same 1,892 requests, one answer each, to the kept build of the base rebuild and to the new build, and explained every answer that differs against the record's edit. Read the first line first.

Behaviour diff: po1qEN54 against q-4zFdlqCU
Changes nobody asked for: 0 of 1,892

Requests: 1,500 drawn at random with the rebuild's seed, 128 from the two records' examples (in 50 of them the old and new oracles answer differently), and 264 from a sweep around the numbers the edit changed. 1,500 + 128 + 264 = 1,892. Unexplained: 0.

flagF {"wetBulbF": 79} whitegreen flagDartLabel: "low"flagDartLabel: "moderate"
  • audit.constants.white_max304 changed, all explained
  • audit.children[n].constants.white_max136 changed, all explained
  • result.flag16 changed, all explained
  • result.flagDartLabel16 changed, all explained
  • audit.result_summary16 changed, all explained
  • audit.children[n].result_summary4 changed, all explained

Real answers, before and after

flagF {"wetBulbF": 78}the first changed examplewhite low→green moderate
flagC {"wetBulbC": 26.66666666666666}wetBulbF=80.0000, now greenwhite low→green moderate
flagF {"wetBulbF": 85}not asked to change, and didn'tyellow high=yellow high

Behaviour diff stored with run po1qEN54ReZWLwQmIakuqQ, against build 1 of q-4zFdlqCU-lLzTaaEy2fg at d843e00. The 78 °F and 26.66666666666666 °C answers are its stored examples; 85 °F is the FL-001 row both commits share. The 79 °F card follows the FL-001 table on each side.

6. The receipt

A signed record you can check yourself

Every rebuild ends with a record signed by graftfork.com with Ed25519. This one names the draft branch, both builds and their verdicts, and the behaviour diff above.

run
po1qEN54ReZWLwQmIakuqQ, passed (a draft)
finished
2026-11-01T02:44:50Z
repository
github.com/craigm26/graft-lab
draft
lab/heat-78, compared with q-4zFdlqCU-lLzTaaEy2fg
commit
c6e9da9982f2
spec
heat-engine 1.5.0-slice
duramen
v0.2.0 (1d48d22)
builders
no network, repository not mounted, no credentials
builds
2 passed, $0.39 and $0.42
suite
215/215, build 1 delivered
behaviour
0 of 1,892 answers changed that nobody asked to change, 6 paths changed
delivered
graft/regen/po1qEN54ReZWLwQmIakuqQ (6f08766a3ba1)
key
4f153e80bf9364d7

Download the signed record (po1qEN54ReZWLwQmIakuqQ.json), then check its Ed25519 signature against the key Graft publishes at /.well-known/graft-record-key. This works today with Node.js 22 or later and nothing to install:

node --input-type=module -e '
import { readFileSync } from "node:fs";
const e = JSON.parse(readFileSync("po1qEN54ReZWLwQmIakuqQ.json", "utf8"));
const { keys } = await (await fetch(
  "https://graftfork.com/.well-known/graft-record-key")).json();
const k = keys.find((x) => x.id === e.key);
const b64 = (s) => Buffer.from(s, "base64");
const pub = await crypto.subtle.importKey("raw", b64(k.public),
  "Ed25519", false, ["verify"]);
const ok = await crypto.subtle.verify("Ed25519", pub,
  b64(e.signature), new TextEncoder().encode(e.payload));
const r = JSON.parse(e.payload);
const b = r.behaviour;
console.log(ok ? "Valid (key " + k.id + "): " + r.run + ", " + r.outcome
  + ", suite " + r.suite.passed + "/" + r.suite.total + ", "
  + b.unexplained + " unexplained of " + b.requests : "NOT valid");'

What it printed for this record (one line, wrapped here):

Valid (key 4f153e80bf9364d7): po1qEN54ReZWLwQmIakuqQ, passed,
  suite 215/215, 0 unexplained of 1892

Change one character of the payload and it prints NOT valid. The record's own fields are the JSON text in payload.

Graft's command line does this in one step: graft verify-record po1qEN54ReZWLwQmIakuqQ.json, or, for a rebuild of your own, by its id with GRAFT_TOKEN set (a draft's record is its owner's alone, so use the file for this one). That needs graftfork 0.3.1 or later (npm install -g graftfork). What graftfork 0.3.0 prints for this record:

Valid: signed by graftfork.com (key 4f153e80bf9364d7).
Draft rebuild po1qEN54ReZWLwQmIakuqQ of branch lab/heat-78,
kept: github.com/craigm26/graft-lab
examples/heat-engine/heat.duramen to
examples/heat-engine/regen/impl/t06-ts (heat-engine 1.5.0-slice),
suite 215/215, 2 build(s). Against rebuild q-4zFdlqCU-lLzTaaEy2fg:
1892 requests, 6 part(s) of the answer changed, 0 change(s) the
record didn't ask for. Started by an agent (token tok_mc-qDIxvjkj5).

One line, wrapped to fit. graftfork 0.3.1 and later say "passed (a draft ...)" for it: a draft never becomes the spec's kept build. 0.3.0 said "kept". More on signed records: Check the signed record.

7. The honest part

You never touch the implementation. You do touch the oracle.

A change in behaviour is a change to the oracle as well as the record: here, two lines of oracle.mjs. The oracle is code, and a person reads and approves it, because the judge believes it. The free check is what keeps the two honest with each other: an edit to the record alone was graded thin with 6 errors, and nothing was built from it.

What you never touch is the implementation. Nobody opened it, edited it or reviewed a line of it for this change. It was written again, blind, from the record, and it passed only because every case and every hidden request agreed.

How rebuilds work · Change the software by changing the record · The diff on GitHub

Second change: a label rename

A prose edit, and not one answer changed

FL-003 was titled "Non-finite °F". We retitled it "Non-finite wet-bulb °F" and bumped the version. Nothing about what the program answers was meant to change, so nothing should.

The whole edit

heat.duramen, from lab/heat...lab/heat-rename

-spec heat-engine 1.5.0-slice+spec heat-engine 1.5.1-slice  …-req FL-003 "Non-finite °F"+req FL-003 "Non-finite wet-bulb °F"

duramen diff on the two records: no tightening, nothing additive, version 1.5.0-slice to 1.5.1-slice: ok (patch). The oracle is untouched.

Check

0 errors

No separate free check is stored for this change. The rebuild checked the record before writing the brief: 22 requirements, 69 examples, 0 errors.

Rebuild

Passed

Both builds passed 215 of 215 cases and agreed with the oracle on 1,500 hidden requests (seed 2091412432). Build 1 passed and waits as a draft.

Cost

$0.86

$0.44 and $0.42 for the two builds, of a $2 cap.

Behaviour diff

0 of 1,760

answers changed. Probes: 128 from the examples, 0 at breaking points, 132 from a sweep.

Compared with the kept build of the unchanged record, 0 paths changed in 1,760 requests, and 0 were unexplained. A retitled requirement is prose; the program it rebuilt answers the same.

Draft rebuild 8ijUl7wBOYkwN21ATgZHgA of commit 64cc436, finished 2026-11-01T06:07:33Z, against build 1 of DgHyeHH8U--OK9lwfiAU_w. Of the 1,500 hidden requests, the oracle itself couldn't answer 1 (a wetBulb request at 1.7976931348623157e+308 °C), and it doesn't count. Download the signed record; the check in step 6 prints Valid (key 4f153e80bf9364d7): 8ijUl7wBOYkwN21ATgZHgA, passed, suite 215/215, 0 unexplained of 1760.

Third change: a new rule

A new reject rule changed exactly what it says

A °C reading sent to flagF by mistake would be classified as °F. The new requirement FL-007 lets a caller say "units": "F", and refuses any other value with bad_request. Without units, flagF answers as before.

The record

heat.duramen, from lab/heat...lab/heat-units

-spec heat-engine 1.5.0-slice+spec heat-engine 1.6.0-slice  op flagF-  input wetBulbF num+  input wetBulbF num, units? string  …+req FL-007 "Units"+  text+    `units` is optional. When present it MUST be `"F"`; any other value is a `bad_request` error, so a °C+    reading sent to `flagF` by mistake is refused instead of classified as °F. Without it, `flagF` reads+    `wetBulbF` as °F, as before.+  example flagF {"wetBulbF": 86, "units": "F"}+    expect result = {"flag": "yellow", "flagDartLabel": "high"}+  example flagF {"wetBulbF": 30, "units": "C"}+    expect error = "bad_request"

duramen diff on the two records: tightening, REQ-FL-007 added; additive, flagF: input units added (optional); version 1.5.0-slice to 1.6.0-slice: ok (minor, needs minor).

The oracle a person approves this

oracle-driver.mjs

-  flagF: (i, c) => O.flagF(num(i, 'wetBulbF'), c),+  flagF: (i, c) => { if (has(i, 'units') && i.units !== 'F') throw new Bad(); return O.flagF(num(i, 'wetBulbF'), c); },

One line, so the judge's model refuses the same requests the record does. As with 78 °F, a change in behaviour is a change to the oracle too.

Check

Strong

23 requirements, 71 examples, 80 evidence rows, 0 errors; 117 planted mistakes caught, 0 missed. Cost: $0.

Rebuild

Passed

Both builds passed 217 of 217 cases and agreed with the oracle on 1,500 hidden requests (seed 2059813404). Build 1 passed and waits as a draft.

Cost

$0.89

$0.45 and $0.44 for the two builds, of a $2 cap.

Behaviour diff

194 of 1,960

answers changed, 0 unexplained. Probes: 130 from the examples, 1 at a breaking point, 330 from a sweep.

Compared with the kept build of the unchanged record, 194 answers changed and every one became bad_request: each was a request with a units other than "F", which the old program ignored. 0 changes nobody asked for. For example, flagF {"wetBulbF": 9007199254740992, "units": "/"} went from black, critical to bad_request.

What the 194 answered beforeAnswersPaths that changed in each, all explained
Every one of them194error, audit.citation, audit.computed_at, audit.function, audit.inputs.wetBulbF, audit.result_summary, audit.spec_version: the answer is now the error and no audit
A null flag (a non-finite wetBulbF)146result, audit.constants
A flag48result.flag, result.flagDartLabel, audit.constants.white_max, green_max, yellow_max, red_max

Free check D1R2EvxTve06fvQyHClInw and draft rebuild 6wwJt3NhpaD7pa7INWuJpg, both of commit e0d21d0; the rebuild finished 2026-11-01T06:15:42Z, against build 1 of DgHyeHH8U--OK9lwfiAU_w. 15 paths changed: 7 in all 194 answers, 2 in 146, 6 in 48. Download the signed record; the check in step 6 prints Valid (key 4f153e80bf9364d7): 6wwJt3NhpaD7pa7INWuJpg, passed, suite 217/217, 0 unexplained of 1960.