Verifiable Trust

How to stop believing what an agent says and start verifying what the system can derive


Introduction: The Five-Action Review

Chapter 20 ended with four words: verifiable trust, never blind trust. This chapter turns those words into an operating procedure. And I want to give you the answer before the architecture, because a trustworthy process should not force the person using it to reconstruct fifteen internal transitions just to review one change.

The current ordinary review path in Gentle AI v2.1.8 is five actions, or four when risk selection chooses no lens:

1. gentle-ai review start
2. run each selected lens once                  # skip when zero lenses
3. produce independent test/requirement evidence
4. gentle-ai review finalize --result ... --evidence ...
5. gentle-ai review validate --gate <gate> --cwd <repo>

That is the happy path. Start freezes the candidate and tells you whether the change needs 0, 1, or 4 read-only review lenses. The selected lenses judge the candidate once. Independent tests or requirement checks produce evidence that did not come from the reviewer merely repeating its opinion. Finalize accepts strict role output and turns it into native authority. Validate re-derives live repository evidence at the delivery boundary. If no lens is selected, action two disappears, but independent evidence does not.

Notice what is NOT on that list. The model does not invent a lineage ID. It does not calculate hashes. It does not serialize operation payloads for fifteen state transitions. It does not freeze ledgers by hand, append event files, update counters, export a bundle, reconcile a mirror, or construct a gate context. Go does the deterministic work. The model does the judgment work. That separation is the center of the chapter.

There is a native authority-first procedure underneath with four facade operations: review start, review finalize, review validate, then optional reconcile-terminal-mirrors after native authority already allows delivery. That fourth operation exists for compatibility and transport. It is not ordinary cognitive work for the model, and it is not another review step for you to remember. If a mirror never needs reconciliation, the review remains complete.

Why insist on the short path first? Because protocol complexity is a reliability problem. Every manual operation is another instruction that can be buried at token 140,000, dropped by compaction, called in the wrong order, or convincingly narrated without execution. The older system encoded valuable invariants, but exposed too much of its plumbing to the probability machine. The current facade keeps those invariants and hides the plumbing behind a deterministic boundary.

Return to our construction inspector. The bad version asks the inspector to pour concrete, maintain the permit database, number every document, calculate every digest, decide which forms apply, and then certify the building. The current version gives the inspector one honest job: inspect the building and return a structured judgment. The municipal system assigns the case number, freezes the plans, validates the form, records the decision, and checks the address when the occupancy permit is used. Better separation, fewer ways to fake competence, less ceremony for the person who only wanted a safe building.

This chapter follows the same progressive path. First you will run the five actions. Then you will see what Go constructs underneath. Only after that will we open causal admission, correction, recovery, publication topology, legacy compatibility, and the real threat model. Happy path first, edge cases later. That is not simplification for beginners. That is architecture respecting cognitive load.

"The model judges the candidate. The facade constructs the authority. The gate re-derives the truth."


The Current Quick Path

Let's walk the procedure exactly as you would use it, without jumping into internal schemas too early.

ActionResponsible partyDurable consequence
review startNative Go facadeCandidate, risk, lenses, genesis paths, and correction budget freeze
Run selected lenses onceRead-only model rolesStrict findings and evidence JSON, no authority bytes
Produce final evidenceTests, build, requirements, runtime checksIndependent bytes ready to bind to the candidate
review finalizeNative Go facadeFindings route, compact state advances, terminal receipt appears
review validateNative gateLive Git and delivery boundary either allow or deny

Action one: start

Run this from any path inside the repository:

gentle-ai review start --cwd .

The facade discovers the repository root instead of trusting your current shell path as authority. It discovers intended untracked files, constructs the immutable target, classifies risk, selects lenses, counts original authored lines, computes the correction budget, derives a lineage, and persists compact state in reviewing.

The response is small and actionable. You get the lineage, state, risk level, selected lenses, changed file and line counts, and the frozen correction budget. You do not get an instruction to manufacture operation JSON. If the risk is low, selected lenses is an empty list. If it is standard, you get one focus lens. If it is high, you get the canonical four in order: risk, resilience, readability, and reliability.

Since v2.1.8, the start response also carries the frozen target identity and one prepared binding per selected lens. The orchestrator prefixes each lens prompt with the exact binding start emitted, instead of typing lineage, target, lens, and order by hand. That sounds like a small convenience. It is an identity rule: if the orchestrator could invent bindings, one transposed character would attach a real judgment to the wrong authority. The party that froze the target is the only party allowed to name it.

That 0, 1, or 4 shape is structural cost control. A docs-only change should not pay for four broad model calls. A normal code change benefits from one focused pass. Authentication, payments, service tokens, security-sensitive paths, or a large change deserve four independent perspectives. The facade makes that choice once from repository evidence and freezes it. A correction cannot recalculate risk downward to escape review or upward to manufacture more work.

Action two: run each selected lens once

Every selected lens is read-only and detached from authorship. It reads the candidate, judges one concern, emits one strict JSON result, and stops. No lens edits files. No lens spawns a fixer. No lens advances lifecycle state. The juror is not the contractor.

A reviewer result looks like this:

{
  "findings": [
    {
      "location": "internal/auth/token.go:84",
      "severity": "CRITICAL",
      "claim": "the candidate accepts an expired service token",
      "proof_refs": [
        "TestExpiredToken passes on base and fails on candidate"
      ],
      "evidence_class": "deterministic",
      "causal_disposition": "introduced"
    }
  ],
  "evidence": [
    "inspected the complete candidate diff and ran the focused differential test"
  ]
}

The omission is deliberate: no finding ID, no lens name, no hash, no lineage metadata. The facade already knows which lens result arrived in which selected position. Native Go fills missing lens and finding IDs, canonicalizes order, validates required proof, and rejects unknown fields. A model is good at claims and evidence. It is a terrible choice for canonical byte construction.

When zero lenses are selected, skip this action. Do not invent an empty reviewer call to make the diagram look symmetrical. The facade knows zero results are required because it froze an empty selected-lens list at start.

Action three: produce independent evidence

Review opinion and verification evidence are different things. A lens can say "the tests look adequate". Evidence says go test ./... exited successfully, the build completed, the acceptance examples passed, or the runtime probe produced the required behavior. The final evidence can be a text file containing command results, a requirements report, or another non-empty proof artifact. It does not need an invented JSON contract.

Independence matters. If the same model says "I reviewed the code" and then writes "tests passed" without a tool result, you have two sentences from one interested party. The facade cannot transform narration into truth. It can only bind actual evidence bytes that another mechanism produced.

Action four: finalize

For one selected lens and a clean candidate, the command can be:

gentle-ai review finalize \
  --cwd . \
  --result reliability-review.json \
  --evidence final-verification.txt

For four lenses, repeat --result in the selected order. Finalize canonicalizes the role output, assigns IDs, routes severe findings by evidence and causality, advances compact state, hashes final evidence, and writes the terminal receipt when the result reaches approved or escalated.

Finalize is resumable. If you provided results but not evidence, it persists validating and tells you to rerun with --evidence. If a candidate-caused blocker requires correction, it persists correction_required and tells you which bounded input comes next. Rerunning the same facade operation does not invent a fresh review budget. Native discovery resumes committed authority.

Action five: validate the delivery boundary

Approval is not permission to deliver anywhere forever. Validate at the boundary you are crossing:

gentle-ai review validate --gate pre-commit --cwd .
gentle-ai review validate --gate pre-push --cwd .
gentle-ai review validate --gate pre-pr --cwd .
gentle-ai review validate --gate release --cwd .

The command auto-discovers compact authority and its receipt, rebuilds the relevant live Git target, and returns a machine-readable allow or denial. It performs zero model calls. A changed candidate, ambiguous lineage, superseded authority, stale destination, missing delivery commit, or moving release evidence fails closed.

A complete clean example

Suppose you changed 60 authored lines across three parser files. Start classifies the candidate as medium risk, selects reliability, and freezes a 30-line correction budget. The lens reads all three paths once and returns no findings plus concrete evidence that it inspected the complete diff. Separately, you run the focused parser suite and the full package tests, saving their output in verification.txt.

Finalize receives one reviewer JSON and the evidence file. Go confirms that one result matches the one selected lens, canonicalizes the empty finding list, moves state from reviewing to validating, hashes the evidence, moves to approved, and writes the receipt. From your point of view that can happen in one command. Internally the facade commits valid intermediate authority, so a crash between transitions can resume without repeating the lens.

Before commit, validate re-derives the current candidate. If you changed one comment after finalization, the synthetic candidate tree or path-bound identity changes and the gate denies. "It was only a comment" is not a cryptographic category. The receipt approved exact content, and the remedy is a fresh authority generation for the changed candidate, not a persuasive explanation.

After committing without changing content, pre-push validation derives the delivered commit and confirms that the committed tree represents the reviewed candidate. The representation moved from worktree/index to commit, but the candidate identity survived. This is what the old manual protocol tried to guarantee with many explicit operations. The facade gives you the guarantee without making you perform the representation transition by hand.

Retry the facade, never reconstruct it

Each facade command has a safe retry story. If review start already persisted a lineage, do not invent another start with copied IDs; inspect the returned authority and use recovery only when its explicit preconditions apply. If review finalize returned "rerun with evidence", rerun finalize with evidence. If it returned "forecast correction lines", provide the forecast before editing. If the terminal receipt already exists, finalize re-discovers it instead of charging another review.

The rule is simple: treat command output as routing, not as prose to reinterpret. The action field tells the orchestrator what legal input is missing. The store_revision tells native code which authority revision exists. The receipt_path appears only at terminal state. A model should not translate "rerun with --evidence" into "start a final-verification transaction" because no such ordinary command exists anymore.

This makes crash recovery boring. Boring is GOOD. You do not ask the model to remember whether it had frozen findings before compaction. You call the same facade, it loads compact state, and it continues from the state that actually reached disk.

Reviewer results that survive failure

The exactly-once lens invocation creates a practical fear: the lens runs once, so what happens when the environment fails around it? v2.1.8 answers with three protections, and none of them weakens verification.

First, capture preflight. Before a lens launches, the facade verifies that its result will have somewhere legal to land: the authority exists, the lineage resolves, the binding matches. The reviewer invocation is precious precisely because it is exactly-once. Failing fast before spending it beats discovering after the judgment that nobody could receive it. Check the recorder before the interview, not after.

Second, explicit repository targeting. A lens can run with its working directory inside a nested repository, and a capture that guesses from the current path would attach the result to the wrong store. GENTLE_AI_REVIEW_CWD pins the repository that owns the review, so nested checkouts stop being an identity trap.

Third, preserved results that cannot pretend. When capture fails after the lens already produced its judgment, the runtime preserves the raw payload as an incident artifact with a deliberately distinct schema. Finalize REJECTS that schema. An unverified payload can never masquerade as a verified capture. Recovery replays the preserved artifact through the full native verification path, the same one a live capture pays. And when the installed binary predates the preservation flag, the runtime degrades gracefully instead of failing the review.

Look at the design shape: every convenience points toward verification, never around it. A safety net that finalize accepted directly would be a second, weaker door into authority. The net exists to carry the result to the same door everyone else uses.

"The safety net must never become a bypass."

This is the procedure you should remember. Everything else in the chapter explains why these five actions are enough and what protections they carry. If an implementation detail later changes while this facade remains stable, your cognitive contract survives. That is what a facade is FOR.


What Go Builds for You

The fastest way to understand the architecture is to draw a hard line between model output and native authority.

The model owns judgment:

Go owns identity and lifecycle:

That boundary removes an entire class of errors. If a prompt tells a model to sort findings, assign IDs, produce canonical JSON, hash it, and place the hash in another object, the system is trusting a text generator to implement a serialization protocol on every run. One omitted empty array, one reordered property, one guessed hash, and two agents can describe the same judgment with different authority bytes. Native code performs the transformation once, under tests.

Start freezes repository evidence

review start resolves the real repository root and builds a snapshot from Git. Intended untracked files are explicit members of the candidate. Historically tracked paths remain review-bound even when a later ignore rule matches them. Genuinely ignored operational state stays outside the publishable snapshot. The result binds base tree, candidate tree, path digest, intended-untracked proof, canonical paths, and a snapshot identity.

The original genesis paths are the immutable correction boundary. A later fix may change content inside those paths, but cannot quietly add a new file because the fixer discovered another idea. The snapshot also feeds risk classification and changed-line counting. Goldens remain in identity, because generated output belongs to the delivery, while generated golden lines are excluded from the authored-line count used for budget math. That prevents a regenerated 5,000-line fixture from manufacturing a 200-line correction allowance.

The correction budget is deterministic:

min(200, ceil(original_changed_lines / 2))

A 40-line candidate gets 20 correction lines. A 900-line candidate gets the hard maximum of 200. The tier, line count, paths, and budget freeze at start. No model can bargain with them later.

Consider intended untracked files carefully. You add schema.json and schema_test.go, but neither is staged yet. A naive snapshot based only on tracked diffs reviews the modified generator and silently excludes both new outputs. The facade discovers the untracked scope and includes their paths and bytes in the synthetic candidate tree. If one disappears before finalization, repository evidence no longer matches. If both move into the index unchanged before pre-commit, validation can prove the representation transition instead of treating staging as scope drift.

Now invert the example. Your local .codegraph/ index changes while the review runs. It is intentionally ignored operational state, not a delivery path. Including it would invalidate authority whenever the index watcher refreshes. Excluding it does not make it worthless; it means its authority belongs to a different domain. Review state itself lives under the Git common directory because linked worktrees need one shared lineage. A CodeGraph index belongs to one checkout because absolute roots and parsed bytes differ. Good snapshots do not include "everything". They include everything in the domain being certified.

Strict schemas keep judgment honest

The runtime exposes versioned schemas through:

gentle-ai review schema reviewer
gentle-ai review schema refuter
gentle-ai review schema validator

Reviewer, refuter, and validator JSON reject unknown fields. Required evidence strings cannot be empty whitespace. Severe findings require evidence class and causal disposition. Refuter outcomes must name an admitted finding. Targeted validation must contain evidence for original criteria and correction regression. Malformed input is rejected before authority changes or correction budget is consumed.

This is a subtle but powerful rule: a schema is not documentation ABOUT the interface. It IS the executable border of the interface. The model may return judgment only in the shape the system can validate. Everything else is prose outside authority.

Finalize constructs canonical authority

When finalize reads role output, Go supplies the selected lens, assigns deterministic missing IDs, canonicalizes each result, concatenates the findings in selected-lens order, classifies severe findings, applies refuter outcomes, derives the correction ID set, and records follow-ups. It does not trust the reviewer to declare which findings are allowed to block.

Why should Go assign IDs? Because an ID is a reference key, not creative content. If four reviewers independently decide whether the first finding is R3-001, reliability-1, or critical-auth, every later validator and correction prompt must absorb naming ambiguity. Native assignment uses selected-lens order and canonical finding order, so the same accepted result produces the same references. The model spends tokens on the claim, where judgment matters, instead of protocol bookkeeping.

Canonical ordering also prevents accidental authority drift. JSON object property order should not change meaning, but array order often does. The facade knows selected lens order and concatenates findings accordingly. It normalizes explicit empty collections rather than letting nil and empty representations fork equality. Then it hashes one accepted shape. Determinism is not about distrusting models personally. It is about refusing to make identity depend on stylistic output choices.

After independent final evidence arrives, Go hashes those bytes, moves compact state to a terminal result, derives the compact receipt, validates it, and writes it atomically. If the command crashes after one persisted intermediate state, the next invocation loads that state and continues. The model does not recreate missing history from memory.

This architecture keeps the prompt small for the same reason Chapter 20 moved giant rule files into skills. The model sees the context needed for judgment, not database maintenance instructions. In PR #1135, the standard review protocol fell from an estimated 10,575 prompt tokens to 1,443, an 86.4% reduction. The four-lens protocol fell from 26,749 to 2,943, an 89.0% reduction. Less context, fewer instructions to lose, and MORE deterministic enforcement.

"Do not ask a probability machine to manufacture canonical authority. Ask it for judgment, then canonicalize judgment in code."


The Compact Receipt and Live Gate Context

Now we can look at the receipt without inventing fields it does not contain. The v2 receipt schema persists exactly these concepts. Tree IDs and hashes are abbreviated below for readability:

{
  "schema": "gentle-ai.review-receipt/v2",
  "lineage_id": "review-9f2c8a41b713e052",
  "generation": 1,
  "base_tree": "21df819000000000000000000000000000000000",
  "initial_review_tree": "d7a29b8000000000000000000000000000000000",
  "final_candidate_tree": "d7a29b8000000000000000000000000000000000",
  "paths_digest": "sha256:9f2c8a41...",
  "fix_delta_hash": "sha256:4f7c0a91...",
  "policy_hash": "sha256:77b0d2ce...",
  "evidence_hash": "sha256:5b1e77aa...",
  "risk_level": "medium",
  "selected_lenses": ["reliability"],
  "resolved_finding_ids": [],
  "terminal_state": "approved"
}

Fourteen fields. No ledger_hash. No transaction_id. No head_event. No repository_id. No destination_ref. Those concepts may exist in compatibility structures or in live gate evaluation, but attributing them to the compact receipt would teach a fictional contract.

What does the receipt actually claim?

Field groupClaim
Schema, lineage, generationWhich compact authority generation emitted this receipt
Base, initial, final treesWhich Git tree boundary was reviewed and which candidate became terminal
Paths and fix deltaWhich original scope and correction delta belong to the result
Policy and evidence hashesWhich native policy and final evidence bytes were bound
Risk and lensesWhich frozen review depth applied
Resolved finding IDsWhich candidate-causal findings entered correction and resolved
Terminal stateApproved or escalated, never an ambiguous prose verdict

The initial and final trees can differ when a bounded correction succeeded. With no correction, fix_delta_hash represents the empty correction identity and the trees remain the same. The evidence hash binds arbitrary final verification bytes. It does not assert that the bytes are true by magic; it asserts that THESE were the evidence accepted by the native transition.

Walk the drift cases. If someone edits a reviewed file after approval, final_candidate_tree stops matching the live snapshot. If someone adds a path, paths_digest stops matching even when every old file remains byte-identical. If policy changes, policy_hash prevents yesterday's approval from inheriting today's rules. If final verification is rerun and produces different bytes, evidence_hash identifies the evidence actually accepted rather than the evidence someone wishes had been accepted. If a correction occurred, initial_review_tree, final_candidate_tree, and fix_delta_hash preserve the relationship between original judgment and corrected delivery.

resolved_finding_ids is intentionally narrower than a full ledger hash. It records the canonical candidate-causal IDs that entered and resolved through correction. The full findings and classifications live in compact state, where the receipt can be re-derived. The receipt stays a compact terminal projection, not a second database that duplicates every field and creates another synchronization problem.

Take one corrected example. Base tree B contains the last delivered source. Initial review tree C adds an auth check but gets the expiry comparison wrong. The reliability lens produces R3-001, native causal admission marks it deterministic and introduced, and the correction produces tree D. The receipt binds B as base_tree, C as initial_review_tree, D as final_candidate_tree, the original path set, the C-to-D fix identity, the final evidence hash, and R3-001 in resolved IDs. That is enough to reconstruct the causal shape without embedding the entire finding object in the receipt.

At pre-push, the gate does not ask the receipt which remote owns D. It resolves the live push boundary, proves the delivered commit has tree D and the expected base relationship, then records that derivation in gate context. One compact receipt can therefore support different gates without pretending their mutable observations were known at review time.

Persisted receipt versus derived gate context

Publication topology changes too quickly and depends too much on the gate to be stamped blindly into the receipt. A pre-commit check cares about the current worktree and index. A pre-push check cares about the push remote, upstream, delivered commit count, and candidate range. A pre-PR check cares about the actual PR base, fork identity, chained topology, and compatible base advances. A release check cares about exact revision and independent release artifacts.

Those values belong to live gate context. review validate derives them from current Git and current inputs at the moment of delivery. The receipt supplies the stable reviewed identity. The gate asks whether today's destination still makes that identity valid.

This separation prevents two opposite mistakes. If you persist a friendly name such as main, it can move while the bytes stay old. If you omit topology validation entirely, a valid candidate receipt can be replayed toward the wrong remote or against the wrong base. Stable facts live in the receipt. Mutable delivery facts are re-derived.

The gate also loads compact state and derives the authoritative receipt from that state. The persisted receipt must match byte-for-byte semantic authority. A receipt file cannot outvote current compact state merely because it parses. A terminal receipt from a superseded lineage cannot authorize delivery. A changed candidate tree or path digest returns scope-changed or invalidated according to the failed boundary.

What a receipt is not

The receipt is not a signature. It is not proof that a particular human approved the candidate. It is not protection against a malicious local actor with the same user account and filesystem access. That actor can rewrite state, receipt, Git repository, or even the executable. Without an external trust anchor, local hashes detect inconsistency and accidental corruption, not authorship.

This is why precise language matters. Say content-bound, state-consistent, CAS-protected against stale writers, and re-derived against live Git. Do not say unforgeable or untamperable. Security grows when you name the actual guarantee, not when you choose the strongest adjective.


Causal Admission Before Correction

A reviewer finding something wrong is not enough to authorize changing the candidate. The candidate must have CAUSED the severe problem. This is the lesson that drove the compact lifecycle.

Imagine a repository with a pre-existing flaky test. Your change edits an unrelated parser. A reliability lens runs the full suite, sees the flaky failure, calls it CRITICAL, and a fixer starts changing test infrastructure inside your parser task. The reviewer found a real problem, but the correction does not belong to this candidate. Without causal admission, review becomes an unlimited scope-expansion machine.

The compact policy therefore separates three questions:

  1. Is the finding severe enough to block?
  2. What quality of evidence supports it?
  3. Did the candidate introduce, activate, or worsen it?

Only BLOCKER and CRITICAL findings enter severe routing. WARNING and SUGGESTION stay informational. Every severe finding needs one evidence class:

Evidence classMeaningRoute
deterministicA test, command, or reproducible before/after proof demonstrates itCorroborate directly, no refuter needed
inferentialThe reviewer has a reasoned claim but no deterministic proofSend all inferential blockers through one read-only refuter batch
insufficientThe claim lacks enough proof to decide safelyMark inconclusive and escalate

And every severe finding needs one causal disposition:

CausalityMeaningMay enter correction?
introducedThe candidate created the behaviorYes, with concrete candidate proof
behavior-activatedExisting dormant behavior became reachable because of the candidateYes, with concrete activation proof
worsenedThe candidate measurably made an existing problem worseYes, with before/after proof
pre-existingThe problem already exists independently of the candidateNo, record a follow-up
base-onlyThe problem belongs to the reviewed base rather than the candidateNo, record a follow-up
unknownCausality cannot be establishedNo automatic fix, escalate

Concrete candidate proof means a changed hunk, candidate-created path, differential test, or before/after result. "This code looks risky" is not causal evidence. "The focused test passes on base tree B and fails on candidate tree C" is.

Why one refuter batch exists

LLM reviewers generate persuasive false positives. A classic example is the alleged lock leak:

"The mutex acquired at line 84 is never released on the error path, causing a deadlock."

The claim sounds specific and severe. Four lines earlier, the function contains defer mu.Unlock(), which executes on every return path. The reviewer pattern-matched manual lock plus error return and missed the defer. If the system immediately authorizes a correction, a fixer will restructure correct concurrency code and create real risk while proudly resolving a phantom bug.

Deterministic findings do not need another model. Run the proof. Inferential severe findings do, but one refuter handles the complete set in a single read-only batch. Whether there are 2 candidates or 20, the protocol pays one adversarial context. The refuter can corroborate, refute, or remain inconclusive for each admitted ID. Refuted claims do not vanish; their outcome remains recorded. Inconclusive claims escalate rather than becoming speculative edits.

Reviewer, refuter, and author stay separate

The reviewer judges. The refuter attacks inferential severe claims. The correction actor changes code only after native causal admission. The targeted validator checks the correction. These are different roles because each has a different incentive and scope.

Do not let the reviewer fix what it found. The moment it can edit, it starts reading toward the repair it already imagines. Do not let the fixer decide which IDs deserve correction. It wants permission to act. Do not let the same role produce the final test evidence solely from prose. Interested parties can contribute observations, but they do not construct the authority that judges them.

Pre-existing and base-only problems still matter. They become non-blocking follow-ups with proof, not deleted findings. That preserves knowledge without hijacking the current delivery. "Never discover anything late" is fantasy. "Never let late discovery mutate frozen scope" is enforceable.

Candidate-causal admission is the difference between a quality system and a cleanup bot. The quality system answers one bounded question: did THIS candidate create a severe problem that THIS correction can resolve inside the original scope?

One diff, four different routes

Imagine a payment change with four findings. First, a new differential test proves the candidate accepts a negative amount. That is deterministic plus introduced, so it enters correction directly. Second, the reviewer suspects a race in an unchanged cache, but has only a code-reading argument. The base exhibits the same behavior, so it becomes a pre-existing follow-up even if the claim is correct. Third, a new timeout appears only under load, but nobody can establish whether the candidate activated it or the environment changed. Causality is unknown, so the review escalates instead of guessing. Fourth, a naming suggestion is useful but non-severe, so it remains informational.

One review produced correction, follow-up, escalation, and information routes. Severity alone could not make those decisions. Evidence class alone could not either. The matrix of severity, proof quality, and candidate causality prevents the most dangerous review habit: treating every confident observation as permission to edit.

The result also explains why a findings ledger cannot be frozen before native admission. Raw lens output is testimony. Canonical findings plus classifications and outcomes are authority. Go constructs that authority after validating every severe claim has exactly one concrete route. The reviewer cannot hide an inconvenient severe finding by omitting classification, and the orchestrator cannot promote a suggestion into correction because it feels easy to fix.

Proof references are not decoration

proof_refs must point to something another actor can inspect: a test name and result, a changed hunk, a command output, a before/after trace, or a concrete path and behavior. "Obvious from the code" is not a reference. "The model is confident" is not evidence. A URL without the relevant assertion is barely better.

For deterministic evidence, the proof should make reproduction cheap. Name the command, fixture, and observed difference. For inferential evidence, state which code path supports the claim and what remains unproven, so the refuter knows what to attack. For causality, compare candidate against base rather than describing candidate in isolation. The system validates that proof strings are non-empty, but humans and role prompts still need to demand useful specificity. Schemas can reject absence; they cannot manufacture epistemic quality.

That is the honest split again. Go enforces coverage and legal routing. Models and tools produce the substance that makes the route deserved.


Bounded Correction and Final Evidence

The clean path moves from reviewing to validating as soon as every selected lens completes with no unresolved candidate-causal blocker. The correction path adds one bounded transaction:

reviewing
  --> correction_required
  --> validating
  --> approved | escalated

That diagram is the successful correction path, not a claim that every failed attempt is impossible. v2.1.8 keeps failed targeted corrections inside the same correction transaction, with cumulative accounting and a maximum of three failed attempts. We will open that edge after the happy path.

Forecast before editing

When finalize returns correction_required, rerun it with a positive forecast before touching code:

gentle-ai review finalize --cwd . --correction-lines 18

The facade checks the forecast against the remaining frozen budget. If the cumulative forecast exceeds min(200, ceil(original_changed_lines / 2)), state escalates before the edit. This is cheaper and safer than allowing a 300-line "fix" and discovering afterward that review scope exploded.

The correction actor receives only corroborated frozen finding IDs and genesis paths. It can organize the work into atomic units with independent rollback boundaries, but those units do not create fresh reviews or fresh budgets. Splitting by invariant still helps: one work unit for snapshot membership, another for publication topology, another for config ownership. The review transaction remains one.

Derive the correction, do not accept a narration

After the edit, the facade builds a fix-diff snapshot from Git. It verifies that every path remains a subset of genesis paths, every ledger ID matches the native correction set, intended untracked scope remains coherent, and actual changed lines come from repository evidence. The fixer cannot submit a hand-written delta claiming it changed only two lines.

The targeted validator returns strict JSON for two questions:

{
  "original_criteria": {
    "passed": true,
    "evidence": ["the original acceptance test passes"]
  },
  "correction_regression": {
    "passed": true,
    "evidence": ["the focused regression test passes"]
  },
  "follow_ups": []
}

The validator is read-only. It does not launch a second broad review and does not add new blocking findings. It proves the original criteria still hold and the correction resolves its regression. Optional observations become follow-ups. If both checks pass and cumulative changed lines stay within budget, compact state moves to validating.

Failed targeted validation is bounded, not erased

What if validation fails? The attempt remains persisted with its snapshot, proposed and actual lines, fix-delta hash, and both validation checks. State returns to correction_required. The original lenses do NOT run again. Frozen finding IDs do NOT change. Risk and genesis paths do NOT recalculate. The next attempt spends from the same cumulative line budget.

Three failed compact correction attempts exhaust the lineage even if their measured delta is zero. Exceeding cumulative changed lines also escalates. And since v2.1.8, an attempt that edits nothing escalates terminally on the spot, because a zero-line fix cannot resolve a candidate-caused blocker and looping on it only narrates progress that does not exist. This is not "three correction rounds" in the old sense, where each round could reopen review and discover fresh scope. It is one correction transaction with up to three bounded attempts to satisfy the same frozen causal findings. That distinction matters.

Put numbers on it. A 120-line candidate receives a 60-line budget. The first correction forecasts 20 lines and actually changes 18. Targeted validation still fails, so 18 lines remain charged. The second attempt forecasts 25 and changes 22. Cumulative use is now 40, leaving 20. A third forecast of 25 escalates before editing because 40 + 25 exceeds the frozen budget. A forecast of 15 may proceed, but if validation fails for the third time, the lineage escalates even if cumulative use stays below 60.

Why count failed work? Because changed code is risk even when it did not solve the finding. Resetting the budget after a failed attempt would reward thrashing. Why forecast as well as measure? Forecast blocks obviously oversized work before it creates a rollback problem; actual measurement prevents an optimistic forecast from laundering a larger delta. Both numbers come from different moments and protect different boundaries.

Work units still need rollback boundaries

One correction transaction does not require one giant edit. If two frozen IDs touch independent invariants, split the implementation into atomic work units that can be reverted separately. Map every unit to admitted IDs, expected paths, focused tests, and runtime evidence or a justified N/A. This reduces review load without pretending each unit owns a fresh correction budget.

Suppose R3-001 requires a token-expiry comparison and R3-002 requires preserving a config header. Implement and test the expiry fix first, then the config preservation. If the second breaks serialization, you can roll it back without discarding the first. The targeted validator still receives the repository-derived aggregate fix snapshot and the complete frozen ID set. Internal work organization improves rollback; native authority judges the final candidate as one correction transaction.

This is why "one correction" and "one commit" are not synonyms. Transaction boundaries come from authority and budget. Commit boundaries come from reviewability and rollback. Good engineering aligns them where useful without confusing them.

Malformed reviewer, refuter, or validator JSON does not count as an attempt because authority never changes. A crash after a valid state replacement can resume from that replacement. A failed validation does count because it represents real repository evidence and real correction work. Budgets should charge effects, not parser mistakes.

Final evidence closes the transaction

Once state reaches validating, final verification evidence is mandatory:

gentle-ai review finalize --cwd . --evidence final-verification.txt

The evidence may contain focused tests, full tests, build output, requirement checks, runtime probes, or an SDD verification report. It is arbitrary non-empty bytes, not fake schema ceremony. Go hashes those bytes and transitions to approved or escalated. A terminal receipt appears only then.

For SDD work, one independent requirements/runtime verification follows the correction logic. If it fails, the result escalates. It does not launch another reviewer, another refuter, or a fresh correction scope. The system converges because judgment happens once, causal blockers freeze once, correction stays budgeted, and final evidence ends in one of two terminal outcomes.

"Bounded does not mean pretending failure cannot happen. It means failure cannot reset scope, budget, or history."


Compact State, CAS, and Recovery

The current lifecycle has five ordinary semantic states:

clean:
  reviewing --> validating --> approved

with correction:
  reviewing --> correction_required --> validating --> approved
                      |                         \-> escalated
                      \-- failed validation --> correction_required

escalated can also occur directly when evidence is insufficient, causality is unknown, a forecast exceeds budget, final verification fails, or correction attempts are exhausted. invalidated exists as a narrow edge state for pristine reviewing authority. It is not a sixth step that every review performs.

Compare that with the old twelve-state diagram. The compact facade does not expose judges_confirmed, findings_frozen, evidence_classified, fixing, fix_validating, ready_final_verification, and final_verifying as model-operated stations. Their useful invariants now live inside native transition functions. A state should exist because the system needs a durable business distinction, not because a prompt needed another checkpoint sentence.

Two files, one expected revision

Compact authority lives under the Git common directory:

<git-common-dir>/gentle-ai/review-transactions/v2/
├── LOCK
└── <lineage-id>/
    ├── review-state.json
    └── review-receipt.json       # terminal only

There is no mandatory events/ directory on the v2 path. review-state.json contains a compact state record with a revision, the SHA-256 identity of canonical state bytes under a domain separator. Every replacement supplies the expected current revision. That pattern is compare-and-swap, CAS: replace value A with value B only if the store still contains the exact revision A you read.

The outer record is intentionally tiny:

{
  "schema": "gentle-ai.review-state-record/v2",
  "revision": "sha256:4d6c...",
  "state": {
    "schema": "gentle-ai.review-state/v2",
    "lineage_id": "review-9f2c8a41b713e052",
    "generation": 1,
    "state": "validating"
  }
}

The real state object contains snapshot, frozen risk inputs, findings, classifications, attempts, follow-ups, and evidence identities. The wrapper revision hashes that complete canonical state, not only the abbreviated fields shown here. Parsing rejects unknown fields, invalid hashes, inconsistent snapshots, unsupported states, non-canonical IDs, and budget arithmetic that does not add up. CAS compares this validated revision before replacement.

CAS solves the stale-writer race. Two processes load revision R. Process one writes successor S and moves the store to revision S. Process two tries to write successor T while still expecting R. The store rejects it. Process two must reload rather than overwrite process one's valid transition. If the exact same operation retries and derives the same state revision, the store recognizes it as idempotent and returns success.

This is stronger than "last write wins" and smaller than event sourcing. Last-write-wins would let the slower process erase a valid correction classification. Full event sourcing would persist every transition and replay a history the ordinary gate does not need. CAS preserves the current valid record, detects concurrency, and lets semantic transition code prove that successor S can legally follow R. It pays for exactly the invariant in scope.

The shared v2 lock serializes writers. State replacement validates schema, semantic transition, immutable scope, and repository-derived evidence before writing. The write uses an atomic temporary file and rename, with filesystem synchronization where practical. Readers see the complete old record or complete new record, not a torn half-JSON after a crash.

That is enough for the in-scope threat model: accidental corruption, interrupted replacement, concurrent writers, stale writers, and repository drift. It is deliberately NOT a local blockchain and does not pretend hash linkage authenticates the person writing the bytes.

Invalidation is narrow

review invalidate can terminally invalidate only pristine reviewing authority. Pristine means no lens results, findings, classifications, outcomes, follow-ups, correction forecast, fix delta, or final evidence. Invalidation retains the original snapshot and records a non-empty reason. It cannot be used to erase a review after findings arrive.

This is an edge operation for a wedged or abandoned start, not part of the five-action quick path.

Recovery creates a successor

Recovery does not mutate or delete the predecessor. gentle-ai review recover creates a distinct successor lineage with generation plus one, records predecessor lineage and exact predecessor revision, disposition, reason, actor, timestamp, and, when required, maintainer authorization.

A scope-changed recovery is explicit enough to audit:

gentle-ai review recover \
  --cwd . \
  --predecessor-lineage review-old \
  --expected-predecessor-revision sha256:abc... \
  --successor-lineage review-new \
  --disposition scope_changed \
  --reason "candidate changed after approval" \
  --actor "maintainer"

The expected predecessor revision prevents recovery from attaching to authority that moved after the operator inspected it. A distinct successor prevents the convenient but dishonest act of rewriting the old generation until it looks current.

The eligible cases are explicit:

Discovery authorizes only a unique valid unsuperseded leaf. A fork with two successors, dangling predecessor, revision mismatch, cycle, or multiple unrelated leaves fails closed. Selecting a superseded lineage can support historical inspection, but cannot authorize delivery.

Recovery is not "try the whole review again until it passes". It is an audited new generation after a terminal or invalid authority, with a distinct lineage and a new frozen budget. The old record remains evidence of what happened.

One real limit remains: snapshots can reference synthetic Git tree objects, and ordinary Git garbage collection may prune unreferenced objects. Durable retention of those trees needs a separate Git-lifecycle design. The compact store does not create hidden refs and should not claim archival durability it does not own.


Repairing Authority Without Weakening Validation

Fail-closed validation has an uncomfortable long-term consequence: the rules get stricter faster than history gets rewritten. A store that lived through several versions can hold entries that were legal when written and invalid today. v2.1.8 met three of them. An incomplete entry abandoned by an old interrupted operation. A recovery edge recorded as unchanged-target under rules that now reject that shape. And escalated edges whose maintainer authorization was captured as free-form text before the exact v1 authorization binding existed.

None of these entries is an attack. All of them fail current validation. And while they sit there, discovery cannot produce a clean answer about the inventory, so a store you need becomes partially unusable for a reason nobody can fix by working harder.

The tempting fix is tolerance. Teach validation to accept the legacy shapes: "if the entry is old, allow the free-form authorization". That is fail-open drift with better manners. Every legacy exception is a permanent door, and a door does not check birth certificates. The entry that qualifies for the exception tomorrow may not be a historical accident.

The fix that shipped goes the opposite direction. Validation stays exactly as strict. Repair becomes an explicit ceremony with evidence:

After the ceremony, validation has learned zero exceptions, the bytes of history are untouched, and the inventory is usable again. Compare it with the land registry. When an old deed predates the notary law, the registry does not start accepting unnotarized deeds. It runs a recognition proceeding: one case, evidence on record, the original document preserved, and a new authorized entry that today's rules fully validate.

The distinction to internalize is between repairing data and repairing authority. Data repair edits bytes until the parser is happy. Authority repair proves what happened, quarantines without rewriting, and re-authorizes under the current contract. The first destroys the evidence it should be preserving. The second is the only kind this system allows.

"Repair is a ceremony with evidence, never an edit."


Publication Gates and TOCTOU

A review receipt answers what candidate reached a terminal result. A delivery gate answers whether that result still applies HERE, NOW, at this boundary.

The current command is always the facade form:

gentle-ai review validate --gate <gate> --cwd <repo>

The old flat gentle-ai review-validate command remains only for legacy v1 compatibility. New documentation, hooks, and ordinary workflows should not use it.

Different gates derive different truth

GateLive evidence it derives
post-applyCurrent candidate scope and intended untracked paths
pre-commitCurrent worktree/index representation still matches compact authority
pre-pushPush remote, upstream, complete delivered commit range, candidate and base
pre-prActual PR base, fork or chained topology, ancestry, compatible base advance
releaseExact HEAD plus complete independent release configuration, generated artifacts, provenance, publication boundary, and freshness evidence

main is a mutable ref, not an identity. origin is a local alias, not a repository identity. "The PR branch" can mean a fork head, a chained base, or a stale tracking ref. The gate resolves object IDs and relationships instead of trusting those friendly names.

For pre-push, a current-changes receipt must represent a real delivered tree change and exactly one delivery commit. The gate refuses an empty delivery or partial range. When the same physical delivery is reachable through more than one ref path, the gate deduplicates the publication range before judging it, so one delivery is validated once instead of double-counted into a false mismatch. For pre-PR, an explicit unavailable base produces a machine-readable denial rather than silently falling back to a convenient branch. A compatible base advance needs its own proof; the old base does not become current because the diff looks similar.

Take a chained pull request. PR B was reviewed against PR A, not against main. While B waits, A merges and main advances to include A. A gate that compares only branch names sees "base changed" and either rejects every legitimate chain or, worse, silently assumes equivalence. The compact gate derives ancestry and candidate tree, then evaluates a compatible base advance with explicit evidence. It can preserve a valid candidate relationship without pretending the old and new base objects are identical. Since v2.1.8 the pre-PR gate derives that compatible-advance proof natively from ancestry instead of accepting an asserted equivalence, and a missing proof keeps the denial.

Now take a fork. Local origin may point to the contributor fork while the PR base belongs to the upstream repository. Replaying a valid receipt against the wrong remote ref can deliver the right bytes to the wrong authority domain. Pre-PR and pre-push resolution stay separate because their questions differ. This is why topology belongs in live gate context rather than a generic destination_ref field guessed at review time.

The next slice must not poison the gate

You approve a slice, commit it, and start the next one on top. Yesterday, that ordinary moment was the most confusing one in the lifecycle: the gates compared your new working tree against a receipt that was finished, honest, and simply about something else, and the result read like an accusation.

v2.1.8 classifies instead of accusing. New work that touches none of the reviewed paths classifies as receipt_unrelated: the old receipt stays valid for what it approved, and the new work needs its own review when it reaches a boundary. New work that reaches into the reviewed scope classifies as receipt_scope_changed: explicit scope action required. Two names, two next moves, no poisoned gate.

The deeper rule hides in the failure path. Classification runs git commands, and git can fail: a corrupted object, an interrupted process, a missing ref. When that happens, the failure propagates as a typed error instead of silently collapsing into one of the two classifications. An infrastructure failure must never be reclassified as an answer. "I could not determine X" and "X is false" are different sentences, and a gate that merges them will eventually deny valid work or, worse, allow invalid work with a straight face.

Chained recovery must rebind delivery

A recovery successor can be born after its predecessor already delivered. Say scope changed after approval, delivery happened through the predecessor chain, and the successor exists to govern what comes next. That successor's receipt is degenerate by construction: base equals candidate and its genesis path set is empty, because at the moment of its creation there was nothing new to review.

Bind that receipt directly at a publication gate and it denies: the delivered commits do not match an empty review. The denial is technically correct and operationally wrong, because the delivery WAS reviewed, by the approved predecessors.

v2.1.8 lets publication gates compose the approved predecessor chain from leaf to root and natively re-derive the delivery: walk the linear history, prove each member's final candidate tree, stitch the per-member genesis segments together, and re-derive under the same lock every other authorization uses. The composition engages only when two conditions hold at once: the direct binding would deny, and the delivered candidate still equals the receipt's candidate. And every failure path inside the composition keeps the original denial. Rebinding exists to rescue a true positive. It is never allowed to overturn a true negative.

Final TOCTOU rechecks

TOCTOU means time of check to time of use. You check candidate C and remote ref R. While validation runs, another process advances HEAD, changes authority, updates the destination, or replaces release evidence. If you authorize using the old observation, every earlier comparison was correct and the final decision is still wrong.

The compact gate therefore performs its full derivation, acquires the shared store lock for final authorization, reloads state, rebuilds the lifecycle snapshot, re-resolves publication refs, verifies the authority graph still has the same unsuperseded leaf, and compares final release evidence when applicable. Any change denies authorization.

That final recheck narrows the TOCTOU window and prevents the gate from authorizing with authority or Git evidence that became stale during its own decision. It does NOT make the later push or publication atomic, and it cannot prove nothing changed after review validate returned and released the lock. Run hooks and CI as close as possible to the protected use. At critical publication boundaries, pin and recheck the immutable identity again immediately before use, or combine validation and use in one transactional operation when the platform provides one.

Hooks and CI are enforcement layers

A local pre-push hook can move validation below model behavior:

#!/bin/sh
if ! gentle-ai review validate --gate pre-push --cwd .; then
  echo "push denied: compact review authority does not match live delivery"
  exit 1
fi

Now the model cannot merely forget to call the gate when Git invokes the hook. But hooks are local. --no-verify, a fresh clone, or another machine can bypass them. A required CI check on a protected branch moves enforcement to the server boundary. Admin override can remain, but it should be an explicit logged human event, not an accidental path around the rule.

Release validation applies the same identity discipline. For Gentle AI v2.1.2, tag, CI, and release workflow all resolve to commit 8fdf1a1d098240b72aaee054186ab8d2dea1a596; six platform archives have SHA-256 digests matching checksums.txt. That is strong integrity and provenance evidence. It still does not create signer authenticity, because the annotated tag is unsigned, and GitHub reports the hosted release as mutable. One boundary can be well verified without every boundary being solved.

A machine-readable denial is operational design, not nicer error copy. scope-changed means the candidate no longer matches and needs new authority. invalidated means state, receipt, base, or another required relationship cannot authorize delivery. escalated means review reached a terminal human-decision boundary. An unavailable pre-PR base should identify boundary selection as the failing stage rather than collapse into generic "review failed".

The orchestrator can route these results without inventing policy. It must not respond to scope-changed by rerunning validate three times, or to escalated by opening another ordinary correction. Reason codes turn failure into a finite state transition for the caller. Prose-only errors invite the same probability machine we removed from authority to reinterpret the gate.


First Publication to an Empty Remote

Every invariant in this chapter sounds finished when you read it. Let me show you one meeting reality, because that collision is the best review-method lesson in the whole release. The feature: pushing to a remote that has zero refs. The first publication of a repository. It took seven adversarial review rounds to ship, and the tool's own review process found every hole.

Why is first publication special? An ordinary push delivers a bounded range: the commits between what the remote has and what you send. A push to an empty remote has no "what the remote has". Git transfers every reachable object: the full history, every commit, every tree, every blob that any commit ever referenced. The invariant "nothing unreviewed gets published" suddenly applies not to a diff, but to everything your repository ever contained.

The first round found a contradiction: validation was skipped entirely for one target kind while being simultaneously over-strict for another. The test fixture passed, but only by construction: it was built in a shape that happened to dodge both defects. A fixture that passes by construction is not evidence; it is a coincidence with a green checkmark.

The next layer was disclosure by path. Path-level comparison asks: is every path in the delivered tree reviewed? Sounds right, and admits a leak. Imagine a secret committed early at config/keys.txt, later overwritten with a sanitized version. The path check inspects the current content and approves. But the push to the empty remote carries the ENTIRE history, including the old blob with the secret. The reviewed tree never shows it; the transferred objects include it.

Blob-level disclosure closed that hole: stop asking about paths, ask about blobs. And then the review found the layer below blobs: metadata. git commit --allow-empty creates a commit with no content change at all, carrying an arbitrary message. Put a secret in that message and every content rule passes, because a message is not content. The object still transfers.

The final invariant landed in layers that match the attack surface:

That last line deserves a pause, because it looks like a weakness and is actually the strongest move in the design. No receipt anywhere binds commit message text. Not on first publication, not on ordinary pushes. Claiming metadata protection would be exactly the kind of adjective inflation this chapter keeps warning about. So the contract states the exclusion, documents it, and pins it with a scope test that must be consciously flipped: whoever extends the contract to metadata must change the test that says it does not, on purpose, in a reviewable diff.

Three lessons worth keeping:

First, invariants meet reality in layers. Paths, then blobs, then metadata. Each fix exposed the next hole, and no single round could have found all three, because each layer only becomes visible once the previous one stops leaking.

Second, an honest contract states what it does NOT guarantee. "Every reachable blob is reviewed content, and messages are out of scope" is more trustworthy than "everything is protected", because you can verify the first claim and only believe the second.

Third, pin contract boundaries with tests. Documentation of a boundary rots silently; a scope test fails loudly the day someone moves the boundary without deciding to.

And enjoy the recursion for a second: the adversarial review machinery this chapter describes is what ran those seven rounds against its own publication gate. The method does not exempt its own code. That is what it looks like when a verification culture actually believes itself.

"An honest contract tells you where it ends, and pins the ending with a test."


From Legacy v1 to Compact v2

This section is history and compatibility, NOT the current procedure.

Legacy v1 used an append-only, hash-linked event chain, per-transition transaction snapshots, a twelve-state ordinary machine, and a model-facing protocol with roughly fifteen internal operations. Through pre-PR, the measured clean lifecycle required 17 to 18 operations. Policy, ledger, evidence, fix-delta, receipt, gate context, and bundle artifacts had to remain synchronized.

That architecture taught important lessons. Content-bound receipts kill stale approval. Immutable genesis paths prevent correction scope expansion. Frozen findings preserve inconvenient evidence. Read-only reviewers separate judgment from authorship. Hashes expose accidental mutation. OS locks and atomic writes prevent stale locks and torn files. Bundles must validate before import. None of those lessons were wrong.

The mistake was exposing internal persistence choreography as ordinary model work and treating a local hash chain as if it strengthened the out-of-scope same-user threat. More operations meant more prompt tokens, more durable writes, more mirrors to drift, and more opportunities for the orchestrator to perform protocol theater instead of review.

The old chain still had a precise accidental-corruption property. Change event three and its content hash changes; event four's predecessor pointer no longer resolves; repairing the suffix changes the chain head. That is useful tamper evidence when an external receipt or trusted copy pins the old head. But a same-user local actor can rewrite the suffix, head, receipt, repository, and validator together. Presenting the chain as local authenticity overstated the threat model while forcing every ordinary transition to pay for history accumulation.

PR #1135 replaced that ordinary path with compact v2 while preserving the useful invariants:

MeasureLegacy ordinary pathCompact v2Change
Semantic states12558.3% fewer
Lifecycle counters120Removed
Durable authority files7271.4% fewer
Per-lineage authority bytes11,4513,11772.8% fewer
Clean authority writes6350% fewer
Automatic bundle exports10Removed
Standard protocol prompt10,575 tokens1,443 tokens86.4% fewer
Four-lens protocol prompt26,749 tokens2,943 tokens89.0% fewer

Compact v2 uses current-state CAS plus a terminal receipt. It does not accumulate event snapshots on the ordinary path. It validates legal successors against the locked current record and repository evidence. It derives live gate context. Mirrors and transport are optional outputs after authority, not prerequisites for authority.

Why are five states enough? reviewing means native scope exists and selected judgment is incomplete. correction_required means canonical causal blockers exist and budgeted work is authorized. validating means review and any correction are complete but independent final evidence has not yet produced a terminal result. approved and escalated are the only terminal meanings ordinary delivery needs. Everything else is data inside those meanings or an edge operation.

For example, "findings frozen" remains an invariant, but it does not need a separate state after Go canonicalizes review output in one atomic successor. "Fix validating" remains work, but the targeted validator result can either return the same transaction to correction_required, move it to validating, or escalate. Removing a state does not remove its rule. It moves the rule into the transition where it can be enforced without another model-visible ceremony.

Legacy commands are read-only compatibility

The flat commands remain so shipped v1 lineages can still be inspected or validated:

review-start
review-step
review-resume
review-validate
review-bundle-export
review-bundle-import

New v1 authority is rejected. Existing v1 history cannot be appended, rewritten, repaired, or migrated in place. review-resume can read it. Legacy validation can continue at delivery gates. Export/import can transport legacy data. That is compatibility, not an invitation to teach the old lifecycle as the happy path.

Compact transport also exists for moving current state and receipt to another clone, but it transports one compact record, not reconstructed event history. Import checks the transport digest, state revision, receipt equality, delivered tree, original base-to-final path scope, and intended-untracked proof. Use it only when authority must travel. Ordinary local review does not require a bundle.

Two implementation scars worth keeping

The v1 work exposed a Go round-trip trap. An empty non-nil slice tagged omitempty serializes as an absent key; unmarshalling leaves the field at nil. reflect.DeepEqual([]string{}, nil) is false even though length, range, and append semantics match. A release gate compared disk-loaded and in-memory values structurally and rejected the common no-untracked-files case. The durable lesson survives v1: after serialization, compare domain meaning, not incidental memory shape.

It also exposed golden-file fan-out. One shared contract sentence generated 13 fixtures. Regenerating one left 12 stale. The rule remains: when one source feeds N generated outputs, regenerate ALL or NONE, preferably with one command and a CI staleness check. Compact prompts reduced that fan-out dramatically, but generated artifacts still need mechanical synchronization.

Good evolution does not deny the old design's value. It identifies which invariants paid rent, moves them behind a smaller interface, and labels the old machinery accurately so nobody learns yesterday's plumbing as today's workflow.


Real Trust Boundaries Beyond Review

Now the honesty clause. Compact v2 protects valid authority from accidental corruption and concurrent writers. It does not authenticate authority against a malicious local actor with the same user and filesystem access. That actor can rewrite review-state.json, review-receipt.json, Git objects, or the gentle-ai binary itself. A local checksum cannot distinguish "the legitimate user wrote this" from "the same user identity rewrote this".

The in-scope guarantees are still valuable:

Think of the trust stack as four separate columns:

PropertyLocal compact v2 providesStronger external anchor
IntegritySchema, hashes, state/receipt equality, live Git comparisonSigned digest or transparency record
Concurrency safetyLock, expected revision, exact retryRemote transactional authority
ProvenanceLineage, generation, snapshot, evidence identityBuild attestation tied to CI identity
AuthenticityNot against the same local userSignature, protected CI identity, hardware-backed key

Do not collapse the columns. A system can have excellent integrity and weak authenticity. That is not failure if the design says so. It becomes failure when documentation calls the whole row "verified" and lets readers assume the strongest meaning.

Authenticity needs an external trust anchor: required server-side CI, signed tags, artifact attestations, hardware-backed keys, a remote transparency log, or another identity the local actor cannot rewrite. Do not ask SHA-256 to answer who acted. Hashes answer whether bytes match.

This boundary rule appears in other parts of Gentle AI, but they are examples, not extra review steps.

Instructions are interfaces. A shell-disabled reviewer must not receive a fallback that requires bash. Give it the MCP operation it can actually call. A bash-capable orchestrator may receive the upstream CLI fallback. The contract must match the receiver's capabilities, or the instruction is impossible prose.

A wrapper owns only its real surface. A safe facade may validate CodeGraph initialization without implementing every upstream intelligence query. When MCP exploration is unavailable, read-only query, explore, callers, or impact should fall back to the upstream CodeGraph CLI, not to a wrapper pretending to be the whole tool. Fallback toward authority, not toward the nearest familiar command.

Preservation obeys schema and ownership. OpenCode's Context7 header contract is Record<string, string>. Replacing the object destroys valid user auth. Copying arbitrary JSON preserves invalid arrays or numbers. The correct mutation keeps schema-valid user-owned headers while replacing only managed transport fields. Preservation without validation launders bad state; validation without ownership clobbers good state.

Managed uninstall is a transaction. Idempotence only says repeated install converges. It does not say who owns a field, which value existed before, or whether uninstall may restore it after a user edit. Safe management records schema-versioned ownership and before-state, rejects drift and symlink escape, stages reversible deletion, restores only what remains owned, and preserves later user changes.

Operational state has scope. Review authority belongs under the Git common directory so linked worktrees share one lineage. A CodeGraph index belongs to one concrete checkout because its root and parsed bytes differ, so each worktree needs its own index under a persistent supported home path. Local and ignored do not mean disposable. They mean "not publishable source content" until a domain says otherwise.

Publication records should preserve history. Security advisories and release corrections should append new scoped statements rather than silently rewriting what was known. Checksums provide integrity. Signed attestations provide stronger authenticity and provenance. Immutable hosting or transparency strengthens publication history. Integrity, authenticity, provenance, and immutability are four questions, not one green badge.

The design rule across every example is the same: identify the authority, define the actor and threat, bind the minimum stable identity, derive mutable context at the boundary, and refuse stronger claims than your trust anchor supports.

Complexity should follow consequence. A weekend prototype reviewed by its author may need a focused test and a simple human check, not compact authority. An agent that can commit, push, open PRs, or cut releases crosses shared boundaries where stale approval and concurrent state have real cost. There the facade earns its weight. The question is never "can we add more verification?" You always can. The question is "which failure class does this mechanism remove, and is that class expensive enough to justify permanent machinery?"

Compact v2 is a useful answer because it reduced permanent machinery while preserving the costly invariants. That is a stronger architectural outcome than merely adding another check.


Conclusion: Trust Is Re-Derivation

Let's close with the procedure again, because the best architecture leaves you with something you can actually run:

1. start native review authority
2. run the selected 0, 1, or 4 read-only lenses once
3. produce independent test and requirement evidence
4. finalize judgment into compact state and receipt
5. validate live delivery at the gate

Four actions when no lens is selected. Three native facade commands. One optional mirror reconciliation after authority, never part of ordinary model cognition.

The model never constructs canonical bytes, hashes, IDs, lineage, snapshot, budget, state, receipt, or gate context. Go does. The model returns strict judgment JSON. Tests and requirements produce evidence. The gate re-derives mutable topology from live Git. That is the clean division of labor.

And here are the rules worth pinning above your desk:

At the start of the chapter, trust looked like a sentence: "the review passed". By the end, trust is a repeatable derivation. Compact state says which candidate and judgment reached a terminal result. The receipt carries the stable identity. The gate asks live Git whether that identity still applies at this delivery boundary. Every layer answers one question it can actually prove.

This book has said it since the first AI chapter: the model is Jarvis and you are Tony Stark. We direct, AI executes. The compact facade adds the engineering clause: neither Tony nor Jarvis hand-writes the flight recorder while the suit is moving. The system records telemetry, and the launch gate checks it against the suit that is actually on the runway.

Use one design test whenever a review protocol grows: ask which parts require judgment and which parts can be derived. Keep claims, causal reasoning, and adversarial reading with the models. Move IDs, ordering, scope, budgets, hashes, state transitions, and live repository checks into native code. Then ask whether every persisted field is stable enough to belong in a receipt or mutable enough to derive at the gate. Those two questions cut through an astonishing amount of agent ceremony.

Your homework is smaller than the old protocol and harder to fake. Pick one agent workflow where approval is still a paragraph. Define the smallest native start, finalize, and validate boundary. Let the model judge inside a strict schema. Let code construct identity. Then make the delivery gate re-derive the world instead of asking the agent what happened.

"Do not trust the story of the review. Re-derive whether this receipt still authorizes this delivery."


References and Resources