Skip to content

gz content

Authoring CLI for the canonical content model substrate (ADR-0.0.34). The gz content command group lets operators import, list, inspect, render, and edit per-turn agent control surface files (rules, skills, personas, chores, handoffs, scenarios, bullets, agent contracts) as canonical Pydantic models.

Synopsis

Bash
gz content <subcommand> [OPTIONS]

Description

gz content is the operator surface for the ADR-0.0.34 rendering substrate. Per the headless-CMS doctrine, every per-turn agent control surface file is rendered byte-stably from a canonical Pydantic model via a Jinja2 template. Operators interact with these models through gz content; output is human-readable prose by default, machine-readable JSON behind --json.

The content type registry exposes eight model types:

Type Surface
AgentContract AGENTS.md, CLAUDE.md
Rule .gzkit/rules/*.md
Skill .gzkit/skills/*/SKILL.md
Chore .gzkit/chores/*/CHORE.md
Persona .gzkit/personas/*.md
Handoff .gzkit/handoffs/*.md
Scenario BDD scenario records
Bullet Single-bullet evidence rows

Round-trip fidelity is binding: model == parse(render(model)) for every type.

Subcommands

import

Read a hand-authored or canonical markdown file, parse it into a Pydantic content model, and emit JSON to stdout. Optionally re-render the canonical form to a target path.

Bash
gz content import <file> --as <type> [--write <path>]

--write persists the re-rendered canonical form to the named path; useful for the OBPI-0.0.34-03 reverse-parse migration workflow.

list

Enumerate the registered content model types from the CONTENT_MODELS registry. Default output is a human-readable two-column table; --json emits a machine-readable array.

Bash
gz content list [--type <content-type>] [--json]

--type filters output to a single type (e.g. gz content list --type Rule).

show

Parse a canonical content file and display a prose summary (type, title, field-by-field breakdown). The operator-facing surface is always human-readable; pass --json for the canonical model_dump_json() form.

Bash
gz content show <file> --as <type> [--json]

render

Parse a canonical content file and emit the re-rendered markdown to stdout. Output is byte-identical to gzkit.content.render.render(model, vendor) for the same input (round-trip stability per OBPI-0.0.34-02).

Bash
gz content render <file> --as <type> [--vendor <vendor>]

--vendor defaults to claude; other vendors render their respective templates.

edit

Open the canonical-form file in $EDITOR (or $VISUAL). On editor save, the temp file is re-parsed and re-validated. Invalid input aborts with the validator diagnostic and never writes a partial file. On successful validation, the original file is atomically replaced with the re-rendered canonical form via Path.replace().

Bash
gz content edit <file> --as <type> [--vendor <vendor>]

The atomic-replace contract means a failed validation (or a non-zero editor exit) leaves the original file byte-identical to its pre-edit state. There is no partial-write state.

remember

Append one addressed, provenanced entry to a surface's append-only corpus store at .gzkit/corpus/<surface>.jsonl and emit a corpus_entry_appended ledger event. This is the write path of the ADR-0.0.37 corpus pipeline (corpus → compress → rendition → playback): capture grows the source of truth; deterministic playback remains the sole writer of rendered surfaces. remember NEVER edits a rendered surface (AGENTS.md, CLAUDE.md, or any mirror) — that is its load-bearing invariant.

Bash
gz content remember <surface> --section <id> --text <text> \
  [--tier invariant|compressible] \
  [--classification Mechanical|Promotable|Judgment|Ambiguous] \
  [--origin <provenance>]

The --section value is normalized to the surface's kebab-case section id (so "Behavior Rules" resolves to behavior-rules). The command fails closed (non-zero exit, no entry written) when the surface is unknown or the section resolves to no template-defined section of that surface — an unaddressable entry is never stored. --tier invariant marks entries emitted verbatim at every compression setpoint; --tier defaults to compressible.

retire

Retire a superseded corpus entry by appending a retraction row whose retires field names the id it supersedes, and emit two ledger events: corpus_entry_appended for the tombstone row, and corpus_entry_retired for the retirement itself. The corpus has exactly one mutation — append — and no delete, so before this verb a superseded operator directive bound the invariant floor permanently and the only escape was hand-editing the append-only store.

Bash
gz content retire <surface> --entry <id> --reason <text> [--attestor <name>] [--origin <provenance>]

Nothing is deleted. The retired row stays on disk with its provenance intact; tier_policy.invariant_entries simply stops returning it, so a rendition no longer has to carry its text verbatim.

Which way the floor moves is a before/after DELTA over invariant-tier liveness — never a property of what kind of row was named. Four outcomes are possible, and the command reports which one occurred:

Outcome When Consequence for a committed rendition
unchanged no invariant entry's liveness moved — the usual case for a routine compressible retirement still satisfies the floor
shrank an invariant entry stopped binding still SATISFIES it
GREW retiring a tombstone revived the entry that tombstone had retired, and that revived entry is invariant-tier (Algebra 6) may now FAIL it
CHANGED both at once — some revived while others stopped binding may now FAIL it

Reading the outcome off the row's tier is the mistake this table exists to prevent: a compressible tombstone over an invariant target GROWS the floor, and an ordinary compressible row moves it not at all.

The command reports which way the floor actually moved, and raises the floor-coherence warning when it grew. Read that line before deciding whether a recompose is needed — the older guarantee that retirement "only ever shrinks the floor" was false for the tombstone case. retire never touches a rendered surface either way.

Retiring a tombstone requires an --attestor only when the entry it revives is invariant-tier — then floor-tier liveness moves, even though the tombstone itself is compressible. A tombstone over compressible content revives nothing that binds the floor, needs no attestor, and reports the floor unchanged. The gate asks what a retirement does to the floor, never what tier the row it names carries.

Corpus attestation (OBPI-0.35.0-02)

A retirement that moves invariant-tier liveness — the 0-Kelvin floor every rendition must carry verbatim — requires a named --attestor. That covers the common case of retiring a tier=invariant entry directly, and it also covers retiring a compressible tombstone whose target is invariant, because that retirement revives the invariant row (see above). Un-binding or reviving floor-tier canon is a canon change either way, and AGENTS.md § Operator Doctrine's ATTESTATION GRANULARITY FOR THE CONTENT SURFACE ruling makes removing an entry attested. Routine retirement that does not move invariant-tier liveness needs no attestor; the attestation guards the floor, not bookkeeping.

--reason is required on every tier. It becomes the retraction row's text and the corpus_entry_retired event's reason, and both surfaces reject an empty one — an empty reason fails gz validate --ledger and leaves a canon row that says nothing. A whitespace-only --attestor or --reason is refused on every tier: whitespace is not attestation.

A retirement that moves invariant-tier liveness without a named attestor fails closed, writing nothing:

Bash Session
$ gz content retire AGENTS.md --entry corpus-attestation-2026-06-06T06:20:27.327411+00:00 --reason "probe"
Error: retiring 'corpus-attestation-2026-06-06T06:20:27.327411+00:00' moves the liveness of invariant-tier entry corpus-attestation-2026-06-06T06:20:27.327411+00:00 — the 0-Kelvin floor every rendition must carry verbatim — un-binding floor canon is a canon change, so it requires a named --attestor (AGENTS.md § Operator Doctrine; the ATTESTATION GRANULARITY FOR THE CONTENT SURFACE ruling); nothing written.
  Retry with `gz content retire AGENTS.md --entry corpus-attestation-2026-06-06T06:20:27.327411+00:00 --reason "<why>" --attestor "<attestor-handle>"`.
$ echo $?
1

Fail-closed paths

The command fails closed (exit 1, nothing written) when --entry names no row in the surface's corpus, when that row is already retired, or when a retirement that MOVES invariant-tier liveness carries no named attestor. Double retirement refuses rather than appending a second retraction, so the ledger carries exactly one retirement witness per retired entry.

Every refusal carries three-part recovery prose — what failed, the cited rule, and a runnable next step. Because no gz verb lists corpus entries for a surface, the command answers that question itself rather than naming one:

Bash Session
$ gz content retire AGENTS.md --entry does-not-exist --reason "probe"
Error: no corpus entry 'does-not-exist' in surface 'AGENTS.md'. Retirement targets an existing entry (append-only corpus store, GHI #635); nothing written.
  Live entry ids include: 'corpus-attestation-2026-06-06T06:20:27.327411+00:00', 'corpus-behavior-rules-2026-06-10T07:53:55.264205+00:00', 'corpus-behavior-rules-2026-06-10T08:12:41.048588+00:00' (+52 more).
  Retry with `gz content retire AGENTS.md --entry <id> --reason "<why>" --attestor "<attestor-handle>"`.
$ echo $?
1

Ledger witnesses

A successful retirement emits both events, appended before retired, so a replay never sees a retirement whose row is not yet witnessed:

Event Carries
corpus_entry_appended the tombstone row's surface, section, entry id, tier
corpus_entry_retired the retired entry id, the tombstone row id, the surface, the retired entry's tier, the attestor, the reason, and the invariant-liveness delta: floor_direction and floor_moved_ids

floor_direction is the fact an auditor needs, not tier. The attestor gate authorizes on whether the retirement MOVED invariant-tier liveness, and tier is only a proxy for that: a compressible tombstone whose target is invariant grows the floor, so an auditor reading tier alone cannot tell an unattested floor revival from a routine retirement. floor_direction is one of unchanged, shrank, grew, changed; floor_moved_ids names the exact invariant entries whose liveness moved. tier remains recorded as the retired row's own tier. attestor is empty on a retirement that moves nothing, which is why the schema declares it without a length floor.

unown

Un-own a corpus-owned section, the one legitimate move that RAISES the decrease-only unowned-byte ratchet (ADR-0.35.0 § Decision item 3: "an undefined reversal path is the one agents invent"). Same corpus-attestation shape as gz content retire, with one deliberate difference: un-owning a section is a canon change every time, so it never reaches the unchanged-canon exemption gz content commit carries forward a standing attestation through — --attestor and --reason are unconditionally required, never conditional on what moved.

Its counterpart is gz content own, the ordinary decrease-or-equal path, which lowers the floor to what the surface measures. gz content remember captures corpus entries and never touches the ownership declaration or its floor (GHI #976 corrected an earlier attribution).

Bash
gz content unown <surface> --section <id> --attestor <name> --reason <text>
gz content unown AGENTS.md --section attestation --attestor "g0" --reason "materialized as prose doc instead"

Corpus attestation (OBPI-0.35.0-04)

Empty or whitespace-only --attestor or --reason fails closed (exit 1), writing nothing — the declaration on disk stays byte-unchanged and no section_ownership_unowned ledger event is emitted (REQ-0.35.0-04-04):

Bash Session
$ gz content unown AGENTS.md --section attestation --attestor "" --reason "probe"
Error: --attestor is empty or whitespace-only.
Why forbidden: un-owning a section is a canon change with the same corpus-attestation shape as `gz content retire` -- it always requires a named attestor and a reason, fail-closed, with no unchanged-canon exemption (REQ-0.35.0-04-04; AGENTS.md § Operator Doctrine). Nothing written.
  Retry with `gz content unown AGENTS.md --section attestation --attestor "<attestor-handle>" --reason "<why>"`.
$ echo $?
1

Given a non-empty attestor and reason against a corpus-owned section, the section flips to unowned and the decrease-only ratchet floor RISES by exactly that section's measured byte span (REQ-0.35.0-04-05):

Bash Session
$ gz content unown AGENTS.md --section governance-doctrine-surfaces --attestor "g0" --reason "materialized as prose doc instead"
Un-owned section 'governance-doctrine-surfaces' of 'AGENTS.md'. Unowned-byte floor rose from 8637 to 10977 (+2340 B). Attested by g0: materialized as prose doc instead
$ echo $?
0

Fail-closed paths

Every refusal prints what failed, why it is forbidden (citing the binding REQ), and a governed next step. Exit 1 reports a precondition refusal or recovery of a different pending section; that recovery can write stores, and an earlier orphan sweep can remove files. Read the diagnostic for what this run did. Exit 2 reports a storage failure or an outstanding recovery obligation; a failed initial durability boundary can refuse before any new transaction starts. Preserve the reported recovery material and follow the named next step.

Exit Refused when
1 --attestor or --reason is empty or whitespace-only
1 <surface> does not resolve to the identity its ownership declaration declares — a different file, a missing file, or a declaration that does not declare itself
1 the surface cannot be read, or is not UTF-8
1 the ownership declaration is missing, unreadable, malformed, or fails load_declaration
1 --section names no id in the declaration
1 the named section is already unowned — there is nothing to raise the floor by
1 the surface's bytes changed between measurement and the commit, checked before either store is touched
1 a pending transition for a DIFFERENT section was completed by this run; the requested section is a separate run
1 the declaration loaded for a FRESH transition declares a different identity than the transaction's target — no journal exists yet, so no declaration byte changed and no witness was appended
2 the measured source could not be retained, or the journal could not be written
2 the declaration could not be written, or its durability barrier failed
2 the ledger witness could not be appended
2 the surface changed DURING the un-owning, after the declaration and the witness were both durable
2 a pending journal cannot be proven to continue the declaration on disk
2 the declaration a pending journal continues is not the CURRENT state of the surface's ownership chain — replaying would mint a competing transition rather than finish an interrupted one
2 the surface no longer carries the bytes a pending transition was measured against — recovery state E below
2 the transition was witnessed by an EARLIER run and the source is unreconciled — the state D+E pair; the witness stands, the claim of clean completion does not
2 THIS transaction's recovery cleanup could not complete — one of its own retained artifacts could not be removed for a reason other than already being gone
2 the journal's absence could not be made DURABLE after this run removed it — its dependent recovery material is preserved untouched
2 the journal's absence could not be made DURABLE on an entry that found none — every orphaned artifact is preserved and no new transaction starts
2 a declaration snapshot consumed under the lock DURING RECOVERY — the on-disk predecessor, the landed declaration, or the witness source — declares a different identity than the transaction's target; the journal is retained, so the transition stays completable

One growth refusal, two states, two recoveries

load_declaration refuses any declaration whose live unowned span exceeds the stored floor, and it does so before the witness chain is read — so unown, which loads the declaration first, refuses in this state whatever section it names. Two different states produce the same arithmetic, and a scalar floor cannot tell them apart: a section hand-flipped from corpus-owned to unowned outside the governed path, or a section that was already unowned and grew. The refusal names every unowned section with its live span, so the grown one is readable off the message, and prescribes a recovery for each state that can act on this declaration (GHI #976):

Bash Session
$ gz content unown AGENTS.md --section prime-directive-ownership --attestor "g0" --reason "probe"
Error: What failed: '.../.gzkit/ownership/AGENTS.md.json' declares unowned_byte_floor 6005, but the summed byte span of its declared-'unowned' sections is 7586, which exceeds it. The 'unowned' sections as the surface measures them now: 'agents-md' (50 B), 'architectural-boundaries' (595 B), 'control-surfaces' (191 B), 'execution-rules' (690 B), 'local-agent-rules' (1758 B), 'pattern-discovery' (383 B), 'persona' (1180 B), 'prime-directive-ownership' (1581 B), 'project-identity' (123 B), 'skills' (688 B), 'skills-first-execution-routing' (347 B).
Why forbidden: REQ-0.35.0-04-02 -- the unowned-byte ratchet is decrease-only. The true unowned span may legitimately sit BELOW the stored floor (a surface shrink before the next ratchet recording), but it may never sit ABOVE it. Two different states produce this, and a scalar floor cannot tell them apart: a section flipped from 'corpus-owned' to 'unowned' outside the governed path (the reproduced attack), or a section that was already 'unowned' and GREW past the floor's headroom. `gz content unown` loads this declaration first and refuses it in both states; `gz content own` reads the floor over the map it produces, so it can act on a grown section but never on an edited map.
Next step: if the declaration's section map was edited, restore the tracked declaration to the state its witness recorded (`git checkout -- .gzkit/ownership/AGENTS.md.json`), then make any intended un-owning through `gz content unown AGENTS.md --section <id> --attestor <name> --reason <reason>`, which raises the floor under a fresh attested witness. If an 'unowned' section grew, either shrink it back under the floor, or own it: capture every content line the corpus does not yet carry (`gz content remember AGENTS.md --section <id> --text "<line>" --tier invariant --classification <Mechanical|Promotable|Judgment|Ambiguous> --origin "<why>"`), then `gz content own AGENTS.md --section <id> --attestor <name> --reason <reason>`, which lowers the floor to what the surface measures. Then retry.
$ echo $?
1

Captured 2026-09-07 in an isolated, git-initialised copy of the repository after prime-directive-ownership was flipped to unowned by hand; the project-root-absolute path is elided to ... and the figures are a dated record, never the current floor. Both prescriptions were then driven through the real command path in that copy:

  • Edited map. git checkout -- .gzkit/ownership/AGENTS.md.json restored the tracked declaration and the loader accepted it at floor 6005; the un-owning the edit was reaching for then landed through the governed verb — Un-owned section 'prime-directive-ownership' of 'AGENTS.md'. Unowned-byte floor rose from 6005 to 7586 (+1581 B). The tracked copy is the state the suite last proved loadable (TestCommittedDeclarationLoadsCleanly loads the committed declaration against the committed surface on every gz check). Neither unown nor own can act on an edited map: own clears the floor relation over its successor map and is then refused because the map on disk is not the one its floor_event_id witnesses — the edit never had a witness.
  • Grown section. With project-identity grown by one line, own refused until the corpus carried every content line and named each uncovered line with the gz content remember capture for it; after four captures, own landed — Unowned-byte floor fell from 6005 to 5882 (-123 B). Coverage: 4 live entries carry 4/4 content lines. — and the loader accepted the result. Reverting the growth instead (the section back under the floor) also loads cleanly; a surface shrink never needs a transition.

Neither branch hand-edits the declaration, and the message never names a verb for a state that verb refuses: that was the defect (GHI #976), the same family as GHI #863.

A witness that is not its chain's tip

A declaration's floor_event_id must be the tip of its surface's ownership chain, not merely a valid point in it. Every check behind that pointer — the id resolves, the type is an ownership type, the surface matches, the recorded floor matches, the whole prefix replays — is satisfied by any prefix of a valid chain, so a declaration restored to an earlier witness used to load while the attested transitions after it were discarded from the declaration and left in the ledger with nothing pointing at them (GHI #979). The floor moved without a governed transition, in both directions: restoring behind an unown drops it, and restoring behind an own raises it through the one move only unown may make.

Bash Session
$ gz content unown AGENTS.md --section gate-covenant --attestor "g0" --reason "materialized as prose doc instead"
Un-owned section 'gate-covenant' of 'AGENTS.md'. Unowned-byte floor rose from 6005 to 9112 (+3107 B). Attested by g0: materialized as prose doc instead
$ git checkout -- .gzkit/ownership/AGENTS.md.json
$ gz content unown AGENTS.md --section attestation --attestor "g0" --reason "probe"
Error: What failed: '.../.gzkit/ownership/AGENTS.md.json' names floor_event_id 'unowned-ratchet-updated-AGENTS.md-owned-stdlib-first-doctrine-dependency-posture-255e275ac942653f', which is not the TIP of 'AGENTS.md''s ownership chain -- 1 attested transition(s) stand after it: 'section-ownership-unowned-AGENTS.md-gate-covenant-281f92a5ef705b2a' (6005 -> 9112).
Why forbidden: REQ-0.35.0-04-02 -- an increase is only reachable through the attested raise-path, which is a claim about the chain's CURRENT state and not about a valid witness existing somewhere in it. Every prefix of a valid chain replays cleanly, so a declaration rolled back behind a governed transition is indistinguishable from one that never advanced: its floor and its section map revert with no governed transition recorded, while the attested events after it stay in the ledger with nothing pointing at them. A valid witness EXISTING behind this floor answers 'is something armed', never 'did the governed procedure run' (AGENTS.md § DO IT RIGHT).
Next step: two states produce this, and they repair DIFFERENT artifacts. (a) The declaration was restored or rolled back over the transition(s) above: recover the copy that names 'section-ownership-unowned-AGENTS.md-gate-covenant-281f92a5ef705b2a' -- `git log -- .../.gzkit/ownership/AGENTS.md.json` lists its revisions and `git checkout <sha> -- .../.gzkit/ownership/AGENTS.md.json` restores one -- then verify the restored copy carries floor 9112. (b) No copy naming 'section-ownership-unowned-AGENTS.md-gate-covenant-281f92a5ef705b2a' survives: no `gz content` verb re-points a declaration at an event already in the ledger -- `own` and `unown` each MINT a new transition, chained from the stale floor and map this declaration still carries -- so that state has NO governed recovery today: stop and escalate (GHI #978).
$ echo $?
1

Captured 2026-09-07 in an isolated, git-initialised copy of the repository; the project-root-absolute path is elided to ... and the figures are a dated record, never the current floor. Both prescriptions were then driven through the real command path in that copy:

  • Arm (a), a saved copy names the tip. With the post-transition declaration committed, git checkout HEAD~1 -- .gzkit/ownership/AGENTS.md.json reproduced the refusal above, and git checkout HEAD -- ... recovered it — the next governed verb then landed, Un-owned section 'attestation' of 'AGENTS.md'. Unowned-byte floor rose from 9112 to 10657 (+1545 B)., with the ledger carrying both raises and neither lost.
  • Arm (b), no saved copy names the tip. Where the transition was never committed, git checkout -- restores the copy that predates it, so the refusal repeats verbatim. That is the honest branch: no gz content verb re-points a declaration at an event already in the ledger, so the message says to stop and escalate rather than naming a verb that cannot act.

No state is exempt: recoverable is not current. The two-store commit writes the declaration before its ledger witness, and the reverse interval — the witness durable while the declaration replacement is lost or rolled back — is completed by the pending-transition journal (§ Recovery protocol). A journal proving that interval makes the state RECOVERABLE, which is a different claim from the one a reader of this loader is making: an interrupted transition is exactly a declaration that is not yet the chain's current state, so it may not be read as one. The journal therefore selects which recovery the refusal prescribes — an executable retry rather than "no governed recovery exists" — and never whether the declaration is accepted:

Bash Session
$ gz content unown Doc.md --section beta-section --attestor "g0" --reason "probe"
Error: What failed: '.../.gzkit/ownership/Doc.md.json' names floor_event_id 'section-ownership-genesis-Doc.md-26', which is not the TIP of 'Doc.md''s ownership chain -- 1 attested transition(s) stand after it: 'section-ownership-unowned-Doc.md-alpha-section-a2b8dac2c44ea20e' (26 -> 83).
Why forbidden: [as above]
Next step: a pending-transition journal at '.../.gzkit/ownership/Doc.md.json.journal' PROVES this gap is this declaration's own interrupted transition -- so this state is RECOVERABLE, which is not the same as CURRENT: the declaration does not become the chain's current state until the transition is completed, and until then it may not be read as one. Complete it: `gz content unown Doc.md --section alpha-section --attestor "g0" --reason "probe"` replays the journalled transition under its own event id 'section-ownership-unowned-Doc.md-alpha-section-a2b8dac2c44ea20e' and writes the successor declaration. Do NOT delete the journal and do NOT hand-edit the declaration.

The journal earns that branch only by being valid on the same terms gz content own/unown replays it under — one authority, read by the loader and the recovery path alike, so nothing admitted here would be refused there as a journal. The two consumers are not interchangeable beyond that, and the message says which one it is: the loader speaks to the CHAIN and never reads the surface, so a source that moved since the transition was measured, or corpus coverage lost since an owning was decided, is met by name when the prescribed verb runs, not here (§ Recovery protocol states A–E). It must carry every field the replay reads, name this surface, carry a non-blank attestation, re-mint its own event_id from its own content, serialize this transition's own successor, start from this declaration's floor and floor_event_id, and its event_id must be the single row standing after them — a row that agrees with the witness the journal describes, not merely one wearing its id. Anything else leaves the two-arm refusal above standing and is named in it. Presence authorizes nothing, and neither does a partial resemblance: the earlier form of this check read four fields, so a journal six fields short of what the recovery requires made the loader accept the stale map and floor while gz content unown exited 2 on the same journal (GHI #979).

Recovery protocol

gz content unown updates two stores — a mutable declaration and the append-only ledger — and neither order is safe alone, so the pending transition is journalled before either is touched. The atomic writer renames and then syncs the parent directory, so every write has a window where the swap landed and its durability barrier did not. Re-running the same command is what completes an interrupted move. Below is what each interrupted state means; every refusal names the state its instruction was derived from.

State What is established Re-running does
A the journal persisted; the declaration is still the predecessor re-validates the journal against the declaration on disk, writes the successor, witnesses it, clears the journal
B the declaration carries the new floor and its floor_event_id; no witness; durability UNCONFIRMED re-establishes the durability barrier BEFORE witnessing or clearing; a persistent barrier failure keeps refusing with the journal retained
C the same shape on disk as B — the two are indistinguishable by inspection the same action; the retry does not guess between them
D the ledger carries the transition's event_id clears the journal and the retained material — only when the source axis is also reconciled
E the surface no longer matches the journalled digest refuses, extracts the retained measured bytes beside the surface, and names the reconciliation
D+E the witness is durable AND the source has moved refuses — the witness is preserved unduplicated, every retained artifact is kept, your edit is untouched, and the exit is non-zero

The predecessor's CURRENCY is a second orthogonal condition (GHI #978). A journal is replayed onto the declaration on disk, and every check above proves it continues that declaration — its floor, its floor_event_id, and the successor derived from it byte for byte. A valid predecessor is not a current one. Like E, the question cuts across A–D: a transition in state A may still be extending a declaration the chain has already moved past, and completing it would chain a second witness from a superseded floor — forking the chain, so every later load fails closed while the run itself reported success. Only two shapes may write: the declaration IS the chain's tip, or the single row standing after it is this journal's own witness (the reverse interval A exists to complete, decided by the same authority the loader reads). Anything else refuses before either store is touched:

Bash Session
$ gz content unown Doc.md --section alpha-section --attestor "g0" --reason "probe"
Error: the pending-transition journal '.../.gzkit/ownership/Doc.md.json.journal' continues '.../.gzkit/ownership/Doc.md.json', but that declaration is not the current state of 'Doc.md''s ownership chain: 1 attested transition(s) stand after its floor_event_id 'section-ownership-genesis-Doc.md-26': 'section-ownership-unowned-Doc.md-alpha-section-6e99068b77f764bd' (26 -> 83) -- and the pending-transition journal does NOT account for that gap: the pending-transition journal would witness 'section-ownership-unowned-Doc.md-alpha-section-b02ac8490d53e081', not the 'section-ownership-unowned-Doc.md-alpha-section-6e99068b77f764bd' standing after this declaration.
Why forbidden: REQ-0.35.0-04-02 -- an increase is only reachable through the attested raise-path, which is a claim about the chain's CURRENT state and never about a valid predecessor existing somewhere in it. A journal may FINISH a transition, never MINT one: completing this would chain a second witness from a floor the chain has already left, forking it, and every later load would then fail closed on that fork -- so the run would report success while creating residue no `gz content` verb can clear (GHI #978). A valid witness EXISTING behind this floor answers 'is something armed', never 'did the governed procedure run' (AGENTS.md § DO IT RIGHT). NOTHING WAS WRITTEN: the declaration is untouched, no ledger witness was appended, and the journal and its retained source at '.../.gzkit/ownership/Doc.md.json.journal.source' are RETAINED.
  Two states produce this and they repair DIFFERENT artifacts. (a) The declaration was restored or rolled back over the transition(s) above: recover the copy that names 'section-ownership-unowned-Doc.md-alpha-section-6e99068b77f764bd' -- `git log -- .../Doc.md.json` lists this file's revisions and `git checkout <sha> -- .../Doc.md.json` restores one -- then verify it carries floor 83. The journal describes a move from a floor that copy has already left, so it can NEVER be completed onto it: move it aside for the record -- `mv .../Doc.md.json.journal .../Doc.md.json.journal.superseded` and `mv .../Doc.md.json.journal.source .../Doc.md.json.journal.source.superseded` -- rather than deleting it, and start any further transition fresh from the restored declaration. (b) No copy naming 'section-ownership-unowned-Doc.md-alpha-section-6e99068b77f764bd' survives: no `gz content` verb re-points a declaration at an event already in the ledger -- `own` and `unown` each MINT a new transition, chained from the stale floor and map this declaration still carries -- so that state has NO governed recovery today: stop and escalate (GHI #978).

Prevention is not recovery. The refusal stops the invalid write; it supplies no verb for residue already on disk, because none exists — own and unown each MINT a transition and neither can adopt one the ledger already carries. Arm (b) says so plainly rather than naming a command that cannot act.

Three obligations, established separately (operator ruling, 2026-09-05). A transition is witnessed, its source is reconciled, and its recovery material is cleaned — and establishing one never discharges the others. E is an ORTHOGONAL axis, not a fifth state: a transition in any of A–D may also have a moved source, and the D+E pair is reported as the pair. A durable witness does not mean the source is reconciled; an attempted unlink does not mean cleanup is complete.

Consequences worth stating plainly:

  • Never delete the journal because the ledger carries no section_ownership_unowned row. States B and C both have an absent witness with the declaration already replaced, so an absent witness does not establish that nothing landed — deleting on that signal destroys the only record able to complete the transition.
  • Never hand-edit the ownership declaration. Its floor must stay witnessed by a real ledger event, and an edited one is refused on the next load.
  • The command never rewrites the surface. In state E it copies the retained measured bytes to <surface>.unowning-recovery and leaves your edit alone.
  • A refusal never hands you a step it cannot keep. When the measured bytes could not be verified and fully extracted, the numbered reconcile sequence is withheld. A failed extract write may leave an older file or a visible replacement whose durability is unconfirmed; the diagnostic names the failed path and the verified retained source rather than claiming the extract is absent.
  • An unreadable or non-UTF-8 live source does not discard pending recovery. The early refusal identifies the journal and retained-snapshot path without claiming it has read or verified that snapshot. Before changing the source, save its raw bytes outside the repository, restoring read access first if needed. Verify the retained snapshot's SHA-256 against the journal's surface_digest before deliberately restoring it, then retry. If verification fails, preserve the evidence and do not overwrite the source.
  • Recovery ends at the restored measured version and a successful retry. Keep newer edits saved outside the repository. Un-owning another section adds the same span to the floor and the live unowned total, so it cannot create headroom for an oversized edit in an already-unowned section.
Bash Session
$ gz content unown Doc.md --section alpha-section --attestor "g0" --reason "probe"
Error: the un-owning of section 'alpha-section' of 'Doc.md' was witnessed by an earlier run, but the surface no longer carries the bytes its floor was measured against: its bytes changed.
Why forbidden: THREE OBLIGATIONS ARE SEPARATE HERE and exactly one is discharged -- transition witnessed; source reconciliation pending; recovery cleanup pending. THE STORES ARE IN § Recovery Protocol state D and the SOURCE IS IN state E -- the two are orthogonal axes, and this exit is the pair. THE TRANSITION DID LAND: the declaration carries the new floor and the ledger carries its witness, and neither is retracted -- an append-only witness is a truthful record of what was committed. But the floor was measured against the surface as it was journalled, so the committed declaration may record a byte span the surface no longer has, and `load_declaration` fails closed while the live span exceeds the floor (REQ-0.35.0-04-05). A durable witness does NOT establish that the source was reconciled, so what is refused here is the claim that this completed cleanly. The journal is RETAINED at '.../.gzkit/ownership/Doc.md.json.journal'. Your edit to the surface is untouched and was NOT reverted.
  The bytes this transition measured are extracted to '.../Doc.md.unowning-recovery', and the immutable original is retained at '.../.gzkit/ownership/Doc.md.json.journal.source'. Reconcile in this order, which preserves your edit and ends with a declaration `load_declaration` accepts: 1. save your current work -- copy '.../Doc.md' to a path OUTSIDE this repository; those bytes stay yours and nothing below reads them; 2. diff that copy against '.../Doc.md.unowning-recovery' to see what moved; 3. restore the measured bytes over '.../Doc.md'; 4. re-run the same command, which clears the journal and the retained material once the surface matches what was measured. Your saved copy is untouched by every step above. RE-APPLYING IT IS A SEPARATE DECISION about the coverage claim and is never part of this recovery: an edit that grows an unowned section past the recorded floor is still refused. Un-owning another section increases the floor and live unowned span equally, so it does not create headroom for that edit. Keep the saved copy outside the repository while deciding how to revise it within the ratchet. Do NOT delete the journal and do NOT hand-edit the ownership declaration -- its floor must stay witnessed by a real ledger event, and an edited one is refused on the next load.
$ echo $?
2

Captured against a small fixture surface by running the command; project-root-absolute paths are elided to .... This is the D+E pair — the transition was witnessed by an earlier run and the source then moved — which is the state that previously reported clean success and destroyed the recovery material.

Recovery artifacts

An interrupted run leaves these behind. They are cleared together once the transition is complete AND its source is reconciled — cleanup is its own obligation, and it is bounded by a durability barrier: the journal's absence is made durable BEFORE any file that depends on it is deleted or reused, and a barrier that cannot be established preserves everything and refuses.

An unsupported or invalid required directory sync (EINVAL, ENOSYS, ENOTSUP, or EOPNOTSUPP) also preserves dependent recovery files and exits 2, including on a fresh entry or a retry that finds no journal. No new transaction starts past that failed boundary. The diagnostic names the attempted location and error; the errno alone does not identify a particular filesystem. Repeating under unchanged conditions cannot establish durability: preserve the recovery material and use an environment where the required directory sync succeeds before retrying. Ordinary transient storage faults retain their repair-and-retry guidance. POSIX uses a directory-descriptor sync; Windows opens the directory and requires a completed native metadata and storage-cache flush. Either operation can refuse when the environment cannot supply it. This guarantee relies on the filesystem and storage honoring the requested synchronization.

Past that boundary the two kinds of failure part company. A removal failure among this transaction's own material keeps what remains and reports the storage fault rather than reporting success. A removal failure on unrelated orphan residue — material that outlived some earlier episode's journal — warns and lets fresh work proceed, because blocking a new transaction on an old leftover reports a fault the new run did not cause. The warning stays attached to the orphan through finalization, so the same leftover is never re-reported as the new transaction's cleanup failing. A warning buys no shortcut: the declaration is still validated and the new transaction's own recovery snapshot is still persisted.

A directory-listing failure leaves its staging-file family unknown, not empty. The diagnostic names the directory, literal filename family, and storage error. Current-transaction cleanup remains non-success. A journal-absent entry may warn and continue, but unknown files cannot be treated as an observed list of old leftovers exempt from the new transaction's cleanup. Restore directory read access and storage health, then retry the inspection.

Path Holds
.gzkit/ownership/<surface>.json.journal the pending transition — both floors, the section, the attestor, the reason, the deterministic event_id, the parent it chains from, and the serialized successor declaration
.gzkit/ownership/<surface>.json.journal.source the MEASURED SOURCE BYTES, immutable for the life of the journal. A digest names the bytes recovery needs; only the bytes supply them
<surface>.unowning-recovery written by a state-E refusal, beside the surface, for you to diff and restore from
.<name>.<random>.tmp beside any of the above the atomic writer's staging file. An interruption mid-write leaves one holding the MEASURED SOURCE BYTES; recovery sweeps them, and .gitignore covers the whole family — final and staging, at every depth, because a surface does not only live at the repository root

The retained source is recovery material, never a second copy of canon — nothing loads from it, and the source surface is never rewritten from it.

Ledger witnesses

A successful raise emits exactly one section_ownership_unowned event, after the declaration write succeeds:

Event Carries
section_ownership_unowned the surface, the section id, the digest of the complete section map that landed, the prior and new unowned_byte_floor, the attestor, and the reason

The event id is DETERMINISTIC — derived from the transition's own content and the floor it chains from — so a retry re-mints the same id and completing an interrupted append is idempotent by construction:

Bash Session
$ tail -1 .gzkit/ledger.jsonl
{"schema":"gzkit.ledger.v1","event":"section_ownership_unowned","id":"section-ownership-unowned-Doc.md-alpha-section-ff1301bc5136427b","ts":"2026-09-05T10:01:23.556123+00:00","surface":"Doc.md","section":"alpha-section","sections_digest":"9b85bce39e25102c6cc439ef0ad84664","prior_unowned_byte_floor":26,"new_unowned_byte_floor":63,"attestor":"g0","reason":"materialized as prose doc instead"}

own

Own an unowned section: the governed lowering move that CHANGES THE MAP (GHI #974; ADR-0.35.0 § Decision item 3 declares the seam two-directional, and OBPI-0.35.0-04 shipped only unown). Once the LIVE corpus carries every content line of the section verbatim, the section becomes corpus-owned and the decrease-only unowned-byte floor falls to the summed span of the sections that REMAIN unowned, as the surface measures them. Same corpus-attestation shape as unown: --attestor and --reason are unconditionally required.

Bash
gz content own <surface> --section <id> --attestor <name> --reason <text>
gz content own AGENTS.md --section stdlib-first-doctrine-dependency-posture --attestor "g0" --reason "corpus carries every line of the section"

Coverage is a state check, never a presence check

compute_baseline calls a section owned when ONE live entry addresses it — the measure REQ-0.35.0-04-08 names as inflated. own does not trust it. Every non-blank body line of the section must be carried verbatim by a live corpus entry addressed to that section (the substring relation the invariant floor already uses, applied per line); a multi-line entry carries each of its own lines. H3–H6 sub-heading lines are structure the surface supplies and are exempt. One uncovered line refuses the whole transition and names the line:

Bash Session
$ gz content own Doc.md --section alpha-section --attestor "g0" --reason "corpus carries it"
Error: the live corpus carries 3 of the 4 content line(s) of section 'alpha-section' of 'Doc.md'; uncovered:
    - '1. **Alpha claim one.** The detail of claim one.'.
Why forbidden: a section becomes corpus-owned only when the corpus carries EVERY content line of it verbatim -- one entry that merely addresses the section is a presence check, and AGENTS.md § DO IT RIGHT forbids a gate whose only witness is that something exists (ADR-0.35.0 § Decision 3; GHI #974). Sub-headings inside the section (1 here) are structure and are exempt. Nothing was un-owned: no declaration byte changed and no witness was appended. That is NOT a claim that this run touched no file -- the entry boundary runs before every check below it, and on an entry that finds no journal it removes the recovery material that outlived one, reporting separately any removal it could not make.
  Capture each uncovered line with `gz content remember Doc.md --section alpha-section --text "<line>" --tier invariant --classification <Mechanical|Promotable|Judgment|Ambiguous> --origin "<why>"`, then retry the same command.
$ echo $?
1

The floor is measured, never subtracted

The new floor is unowned_span_total over the successor map on the LIVE surface — the same arithmetic load_declaration reads. prior - span would assume every other unowned section still has the span it had when the prior floor was recorded, and the state own exists for is the one where an unowned section GREW. The loader reads the floor relation over the successor map for this verb (sections_becoming_owned), so an overgrown surface that the loader otherwise refuses can still be brought back under its floor — the loader's growth refusal names this verb for exactly that state (see unown § One growth refusal, two states, two recoveries); a remainder still above the stored floor is refused, because this is the ordinary, decrease-or-equal path and may never raise the ratchet (REQ-0.35.0-04-02):

Bash Session
$ gz content own AGENTS.md --section make-llm-stochastic-vibes-inert-anti-vibing-mantra --attestor "g0" --reason "corpus carries all content lines"
Owned section 'make-llm-stochastic-vibes-inert-anti-vibing-mantra' of 'AGENTS.md'. Unowned-byte floor fell from 8637 to 7663 (-974 B). Coverage: 6 live entries carry 6/6 content lines (1 sub-heading line(s) exempt as structure). Attested by g0: corpus carries all content lines
$ echo $?
0

(Observed 2026-09-07 in an isolated fixture copy of the repository; the figures are a dated record, never the current floor.)

Fail-closed paths

Exit 1 refuses before either store is touched; exit 2 reports a storage failure or an outstanding recovery obligation and RETAINS the journal.

Exit Refused when
1 --attestor or --reason is empty or whitespace-only
1 <surface> does not resolve to the identity its ownership declaration declares
1 the surface or the corpus cannot be read, or the declaration fails load_declaration on any check other than the floor relation over the successor map
1 --section names no id in the declaration, or is already corpus-owned
1 the live corpus does not carry every content line of the section, or the section has no content lines
1 the sections that would remain unowned exceed the stored floor — the loader names them
1 the surface's bytes changed between measurement and the commit
1 a pending transition for a different section, or a pending un-owning, was completed by this run; the requested owning is a separate run
2 any storage failure or recovery state listed under unown § Fail-closed paths — the machinery is shared
2 a pending owning's section is no longer fully carried by the corpus (an entry was retired in between): § Recovery Protocol state A with the corpus moved; capture the named lines and re-run

One journal, two verbs

own and unown share the declaration lock, the entry-time recovery boundary, the retained measured source, the pending-transition journal, the two-store commit, § Recovery Protocol states A–E and the cleanup. A surface has ONE pending transition; whichever verb runs next completes it, reports it, and exits 1 without starting its own — the journal's transition field ("own", absent for an un-owning) selects the direction. The recovery artifacts and their .gitignore rules are the ones documented under unown.

Ledger witness

A successful owning emits exactly one unowned_ratchet_updated event — it IS the decrease-or-equal move that type witnesses — after the declaration write:

Field Carries
surface, section the surface and the section that became corpus-owned
sections_digest the digest of the COMPLETE map that landed
prior_unowned_byte_floor, new_unowned_byte_floor the floor pair; new is at or below prior
predecessor_event_id the ownership event this owning chains from
attestor, reason the attestation
covering_entry_ids, covered_lines, body_lines the coverage evidence the owning rested on, as journalled

record_unowned_total witnesses map-INVARIANT lowering under the same type with none of the last four groups; load_declaration holds any ratchet row whose map differs from its predecessor's to the section, attestor and reason (_refuse_unattested_map_change). The event id is deterministic and chained (unowned-ratchet-updated-<surface>-owned-<section>-<digest>), so a retry re-mints the same id and completing an interrupted append is idempotent.

reconcile-retirements

Append a Layer-2 witness for corpus retirements that have none. This is the repair arm of gz validate --corpus-retirement-witness (GHI #885, GHI #878).

Text Only
gz content reconcile-retirements <surface> [--reason <text>] [--dry-run]

A retraction row is a canon change: Corpus.retired_ids() folds the on-disk pointer and the target leaves the effective corpus. Two paths leave that change with no ledger witness — a row appended by hand, so gz content retire never runs (GHI #885), or the verb dying between its corpus write and its ledger appends (GHI #878). Both leave the same signature, and this verb repairs both.

It emits corpus_retirement_reconciled, never corpus_entry_retired. That distinction is the point. Backfilling the governed type would stamp today's timestamp — and, on the invariant floor, an attestor — onto a procedure nobody performed, which AGENTS.md § Attestation calls a fabricated receipt. Re-running the governed verb is not available either: retire fails closed on an already-retired id. So the only honest record is a different sentence — a tombstone was found without a witness and accounted for on this date — and that is the only sentence this verb writes. Because the two types stay separate, an auditor reading Layer 2 can still tell a governed retirement from a reconciled one long afterwards.

Idempotent. It emits only for tombstones the witness gate still reports, so a second run over a reconciled surface writes nothing and exits 0.

Observed on the gzkit repository, 2026-08-26, repairing the seven rows GHI #885 found (elided to two for length):

Text Only
$ gz content reconcile-retirements AGENTS.md --dry-run
AGENTS.md: 7 unwitnessed retirement(s) would be reconciled:
  corpus-operator-doctrine-verbatim-canon-2026-06-19T22:54:19.779516+00:00
    via row corpus-retraction-...-2026-08-22T20:21:54.365168+00:00  origin='GHI #862; operator ruling 2026-08-22'
  ...

$ gz content reconcile-retirements AGENTS.md
reconciled corpus-operator-doctrine-verbatim-canon-2026-06-19T22:54:19.779516+00:00
...

AGENTS.md: 7 reconciled, 0 still unwitnessed.

The retraction row's origin prose is carried onto the event: it is the only surviving forensic difference between a governed and a hand-written tombstone, so the repair preserves it rather than overwriting it with its own provenance.

Exit 0 when the surface is fully witnessed (before or after the run), 1 when the surface has no corpus store, 3 when a row selected for repair survives its own repair — a state that means the event did not bind to the subject the gate reads, and is reported loudly rather than as a clean exit over a still-red gate.

compose

Stage a candidate rendition from the corpus. This is the compress stage of the ADR-0.0.37 CMS pipeline (corpus → compress → rendition → playback), in two modes selected implicitly by the arguments given — never by a flag:

Mode Selected when What it does
Explicit (OBPI-0.0.37-21) --candidate <file> given, OR no --candidate and stdin is piped/redirected (not a tty) with real (non-whitespace) content The agent (wielding gz-content-compose) supplies the candidate text; the tool validates invariant-tier verbatim preservation and computes per-tier byte evidence
Generated (OBPI-0.35.0-05) no --candidate AND (stdin IS a tty, OR stdin is piped/redirected but empty/whitespace-only) The candidate is DERIVED from the corpus itself: corpus-owned sections are materialized from the effective corpus, unowned sections are carried forward byte-verbatim from the prior committed rendition (.gzkit/ownership/<surface>.json — see own/unown)

A tty stdin is never read; empty/whitespace-only non-tty stdin IS read, once, to learn it carries no caller-supplied text. The generated path's whole purpose is to let an operator run gz content compose AGENTS.md --consumer root at an interactive terminal without it hanging on a stdin read, AND to let a non-interactive caller — CI, a script, a behave scenario — redirect from /dev/null and land on the same path rather than composing against an empty string: empty stdin IS "no caller-supplied text." The explicit path's cat candidate.md | gz content compose ... idiom is unchanged.

Both write the candidate to .gzkit/renditions/<surface>/<consumer>.candidate.md and emit a composition_candidate_emitted ledger event. The generated path additionally writes the staged provenance map .gzkit/renditions/<surface>/<consumer>.candidate.lineage.json — the bare {section_id: {owned, entry_ids, byte_span}} map (ADR-0.35.0 Decision 5) naming, for every section, whether it was corpus-owned or carried forward, which corpus entry ids contributed its bytes (empty for a carried-forward section), and its half-open UTF-8 byte_span in the candidate text. It is staged alongside the candidate — never overwriting the prior consumer's committed lineage — and is a separate artifact from RenditionProvenance (<consumer>.corpus.json, frozen, written at commit time): the lineage map is generate-time and per-section, the provenance sidecar is commit-time and per-artifact.

Byte evidence: two measurements, never one

compose prints two separately labelled accountings, because they measure different things and blending them produces false refusals:

Line What it measures Reconciles to total_bytes?
Byte evidence (population): Summed text bytes of the effective corpus entries, by tier. A population statistic — two entries whose texts overlap in the rendition are each counted in full, so this can legitimately exceed the candidate's size No, and it is not meant to
Rendered bytes (assembled): The three disjoint byte ranges the generator actually wrote — emitted (corpus entry text placed into owned sections), structural (the headings and separators the generator itself wrote), carried (unowned section bytes copied byte-verbatim from the prior rendition) Yes, exactly

The rendered line appears on the generated path only. The explicit path receives finished text from the caller and places no byte itself, so it has nothing to measure during assembly and reports no rendered partition rather than manufacturing one by subtraction.

Observed against gzkit's own AGENTS.md corpus on 2026-09-09 (a dated record, not a live figure). compose prints the two path lines as absolute paths; they are shown here rooted at <project-root> so the example reads the same on any machine.

Bash Session
$ uv run gz content compose AGENTS.md --consumer root < /dev/null
Candidate: <project-root>/.gzkit/renditions/AGENTS.md/root.candidate.md
Lineage: <project-root>/.gzkit/renditions/AGENTS.md/root.candidate.lineage.json
Byte evidence (population): invariant=24350B compressible=354B→354B total=31244B setpoint=lite
Rendered bytes (assembled): emitted=24704B structural=535B carried=6005B total=31244B

Why the split is load-bearing. An earlier revision derived the structural figure as total_bytes - invariant_bytes - compressible_bytes_after — a rendered remainder computed from a population total. Two distinct live invariant entries whose texts overlap (one a suffix of the other) sum to more than the span carrying both, so the remainder went negative and the generator refused a perfectly valid candidate. The brief's Generation and Accounting Contract had already ruled it out: entry-text totals "remain separately labeled population statistics, never a claim of unique rendered-byte coverage."

Both modes are deterministic — NO LLM call, NO network I/O. On the explicit path the drop/combine/rewrite compression judgment is the agent's; on the generated path there is no judgment to make, because the tool derives every owned byte from the corpus itself. compose NEVER writes a rendered surface (AGENTS.md, CLAUDE.md, or any mirror) — only the candidate artifact, its lineage (generated path), and the ledger change.

Bash
# Explicit: agent-authored candidate file
gz content compose <surface> --consumer <vendor> --candidate <file>
gz content compose AGENTS.md --consumer root --candidate /tmp/candidate.md

# Explicit: piped/redirected stdin with real content (not a tty -- still the explicit path)
cat /tmp/candidate.md | gz content compose AGENTS.md --consumer root

# Generated: no --candidate, run at an interactive terminal
gz content compose AGENTS.md --consumer root

# Generated: no --candidate, non-interactive with no stdin content (CI, scripts, behave)
gz content compose AGENTS.md --consumer root < /dev/null

The command fails closed (non-zero exit, no candidate and no lineage written) when: - the corpus store for <surface> does not exist, - the (surface, consumer) setpoint is undeclared in data/vendor-manifest.json, - the candidate drops or rewrites any tier: invariant corpus entry (0-Kelvin floor) — both paths, - <consumer> is not on <surface>'s declared content-type route — generated path, - two LIVE tier: invariant corpus entries share byte-identical text — generated path (never silently deduplicated; retire one via gz content retire), or - no prior committed rendition exists for (surface, consumer), or an unowned section's bytes precede the rendition's first H1/H2 heading (preamble) — generated path.

commit

Promote a staged candidate to the durable committed rendition under operator attestation. This is the governed candidate→committed seam of the ADR-0.0.37 CMS pipeline: compose stages <consumer>.candidate.md; commit writes <consumer>.md AND freezes the corpus content-fingerprint in a provenance sidecar <consumer>.corpus.json, then emits a rendition_committed ledger event.

commit carries the corpus attestation (NOT Gate 5) — and the attestation attaches to the canon change, never to this Layer-3 re-render. --attestor and --attestation-text fail closed when empty only if the corpus moved since this consumer's last committed rendition; a re-render of unchanged canon needs no attestation and carries the standing one forward (GHI #821). The discriminator is the corpus fingerprint the sidecar already froze. A first commit — no sidecar — is always attested: an absent sidecar is not evidence that canon is unchanged. The operator's verbatim --attestation-text IS the corpus attestation (mirrors gz obpi repudiate). The frozen fingerprint is exactly what gz validate --rendition-freshness compares the live corpus against: when the corpus drifts from the committed rendition, the freshness gate flags it and the recovery is to recompose and re-commit.

commit is not the last step. It writes the rendition — playback is the sole writer of the rendered surface itself, so a canon change is only applied once gz agent sync control-surfaces runs. Until it does, gz validate --invariant-coherence fails closed (exit 3) with the pending diff as its message: the committed rendition and the played-back AGENTS.md disagree. The command's success output names this next step.

Bash
gz content commit <surface> --consumer <vendor> --attestor "<name>" --attestation-text "<verbatim>"
gz content commit AGENTS.md --consumer root \
  --attestor "g0" --attestation-text "attest completed"

# Re-render of unchanged canon (a trim, a recompose): no attestation needed.
gz content commit AGENTS.md --consumer root

# A candidate that removes a prior block: --retention-map required.
gz content commit AGENTS.md --consumer root --attestor "g0" \
  --attestation-text "C2 drop accepted" --retention-map /tmp/agents.retention.json

The command fails closed (non-zero exit, nothing written) when: - --attestor or --attestation-text is empty or whitespace and the corpus fingerprint differs from this consumer's committed sidecar, or no sidecar exists, - no staged candidate exists for (surface, consumer), or - no corpus store exists for <surface> (nothing to fingerprint).

The retention gate above runs after these checks and before any write — it never weakens one of them, it only adds its own (Requirement 8).

Retention gate: --retention-map (ADR-0.35.0 § Decision item 10)

When the candidate about to be committed drops a block the consumer's prior committed rendition carried (rendition_path(root, surface, consumer) — never the staged candidate), promotion requires a --retention-map <file> accounting for every condition of every removed block, or the commit exits 3 and writes NOTHING: no rendition, no provenance sidecar, no retention sidecar, no ledger event. This is the mechanical floor the 2026-09-17 compression skipped when it dropped 23 binding conditions and every check passed (GHI #1090, #1091): the tool proves bytes, never meaning (see § Named residual below).

A block is a markdown heading, paragraph, list item or table row, as split in the rendered surface; a fenced code block counts as one block. A prior block is removed when its LF-normalized, trailing-whitespace- stripped text is not a substring of the candidate. Moving or reordering a block never removes it.

Vacuous cases — no --retention-map needed, commit succeeds exactly as before this gate existed: - no prior committed rendition exists for (surface, consumer) (first commit) — a missing prior is checked by the committed file's absence, never by an empty delta after a failed read, because a missing prior is never evidence that nothing was lost, - a byte-identical re-render, - a candidate that only adds or reorders blocks, - a whitespace-only difference from the prior rendition.

An existing but unreadable prior committed rendition is a system error, exit 2 — never silently treated as vacuous.

The map JSON schema — surface and consumer (both must equal this invocation's surface and --consumer: a map written for one pair never governs another pair's promotion, so a mismatch is a violation), extracted_by (the independent reviewer's identity), mapped_by (the author's identity, and it must differ from extracted_by after case-folding and trimming), and blocks[]. Each block entry carries removed (the removed block, verbatim), conditions[] and non_binding[]: - a condition is {id, quote, disposition: kept|dropped, span?, reason?} — id is a short human-typable token matching ^[A-Za-z][A-Za-z0-9_-]{0,15}$ (e.g. C1, C2), unique within the whole map; quote must be a substring of removed; a KEPT condition names span, a substring of the candidate; a DROPPED condition names a non-empty reason; - a non_binding entry is {quote, reason} — a sentence of removed that binds nothing, named with a non-empty exemption reason. An empty or whitespace-only reason is a violation, because an exemption with no stated reason would drop a sentence without either a DROPPED id or a rationale the operator can read.

Minimal example:

JSON
{
  "surface": "AGENTS.md",
  "consumer": "root",
  "extracted_by": "reviewer-agent",
  "mapped_by": "author-agent",
  "blocks": [
    {
      "removed": "- `--accept-uncovered` is refused on every lane.",
      "conditions": [
        {
          "id": "C1",
          "quote": "`--accept-uncovered` is refused on every lane.",
          "disposition": "dropped",
          "reason": "superseded by the retention gate itself"
        }
      ],
      "non_binding": []
    }
  ]
}

Coverage — every meaningful character of removed (any non-whitespace character except markdown markup *, _, `, #, |, > and the block's leading list marker) must lie inside some conditions[].quote or non_binding[].quote; symbols such as <=, % and -- count because they change meaning. Coverage is reported per sentence, naming the uncovered text — a sentence may be split across several conditions whose quotes together cover it, but a quote naming three words of a sentence leaves the rest of that sentence unaccounted for. Every map entry is validated: a map entry naming no removed block of this delta, or two entries for the same removed block, is a violation — no entry is silently ignored.

Exit 3 names every violation in one refusal, never only the first, each with the removed block's first line, and a three-part recovery (.claude/rules/guardrail-feedback-prose.md): what failed, why it is forbidden (citing ADR-0.35.0 § Decision item 10 and GHI #1090/#1091), and the runnable next step — account for every condition of each removed block in --retention-map, mark each KEPT with its verbatim candidate span or DROPPED with a reason, name every DROPPED condition's id in --attestation-text, then re-run gz content commit. Exit 1 is reserved for a malformed map (including non-UTF-8 bytes or an id that fails the id pattern); exit 2 is reserved for an unreadable prior committed rendition, including one whose bytes are not valid UTF-8.

A DROPPED condition's id must appear, at a token boundary, in this invocation's --attestation-text — a carried-forward standing attestation (the unchanged-canon exemption above) never counts, because the operator rules on every drop in the words supplied WITH THIS commit.

On success, the validated map is written to .gzkit/renditions/<surface>/<consumer>.retention.json, in the same transaction order as the rendition and provenance writes. The next promotion overwrites it — history lives in git beside the rendition. A promotion with no removed blocks removes any stale retention sidecar, so a sidecar never describes a delta it did not govern. The success output lists every correspondence and exemption the tool cannot judge, so the operator can read what happened without opening the sidecar:

Text Only
C1: "<quote>" -> "<span>"
C2: DROPPED -- <reason>
NB: "<quote>" -- NON-BINDING: <reason>

Named residual (Requirement 9) — the tool proves bytes, never meaning. The gate cannot prove four things, and each is held by something other than the tool:

  • That the reviewer extracted every sub-clause condition. The sentence-coverage floor bounds this at clause level: every meaningful character of a removed block must lie inside some quote.
  • That a KEPT span carries the same meaning as its quote. A KEPT span is checked only for byte presence in the candidate, never for semantic equivalence. The independent reviewer verifies each pair, and the success output prints every pair so the operator can read it.
  • That a DROPPED id in --attestation-text came from the operator. The rule against fabricating operator words holds this.
  • That the reviewer was a different model rather than a different name. This is left to the skill's dispatch record (.gzkit/skills/gz-content-compose/SKILL.md).

Whether a non_binding sentence truly binds nothing is likewise the reviewer's and the operator's judgment, never the tool's.

land

Land a corpus change on every consumer routed for <surface> in data/vendor-manifest.json as one governed transaction (ADR-0.35.0 § Decision item 6, OBPI-0.35.0-07). land generates each consumer's candidate from the corpus, verifies it against the surface's ownership declaration, passes it through the retention gate, and then publishes each consumer's committed rendition (<consumer>.md), provenance sidecar (<consumer>.corpus.json), lineage artifact (<consumer>.lineage.json) and, when a retention map applies, retention sidecar (<consumer>.retention.json) under .gzkit/renditions/<surface>/. It emits one rendition_landed ledger event carrying the full target/hash manifest.

Bash
gz content land <surface> [--attestor <name> --attestation-text "<verbatim>"]
                          [--retention-map <file> ...] [--dry-run]
gz content land <surface> --status <landing_id>

<surface> is required; there is no default surface, matching compose and commit. Omitting it is a usage error (exit 2):

Text Only
$ uv run gz content land
BLOCKERS: gz content land: error: the following arguments are required: surface
Option Meaning
<surface> Control surface to land (e.g. AGENTS.md); every consumer routed for it lands together
--attestor <name> Operator attesting the corpus delta; required only when canon moved
--attestation-text <text> Operator's verbatim corpus-attestation token; same conditional requirement. Also where every DROPPED retention condition id must appear
--retention-map <file> Retention map JSON bound to ONE consumer by the map's own consumer field; repeat once per consumer whose candidate removes a block
--dry-run Print the landing plan (targets, hash prefixes, actions, attestation source) and write nothing; over an in-flight landing, print what resume would still publish
--status <landing_id> Classify each consumer of that landing as new, old or indeterminate by content hashes; read-only
Exit When
0 Landed or resumed; with --dry-run, planned; with --status, classified (including indeterminate consumers: the verdict is the output)
1 User/config error: empty attestation on a new delta, unreusable evidence, unrouted surface, duplicate or malformed --retention-map, inputs changed after preparation, a malformed journal, a resume refused over drift or a foreign edit, an unknown --status landing id
2 Usage error (missing <surface>), or system/IO error including incomplete publication: the journal is retained and no completion event is recorded. This includes a target edited outside the landing after its checks ran (during staging, or after resume's checks): publication stops at that file without overwriting it
3 Retention gate refusal: a removed block is unaccounted for, or a --retention-map is bound to no routed consumer of the surface

Every refusal prints three-part recovery prose (Error: / Why forbidden: / Next:), and every preflight refusal writes nothing for any consumer: no journal, no rendition, no sidecar, no ledger event.

One corpus attestation, one landing_id, N consumers

land takes exactly one corpus attestation, on the corpus delta, for all N consumers. Renditions are Layer-3 views generated deterministically from the corpus, so N attestations would demand N human judgments where only one exists. --attestor and --attestation-text fail closed when empty or whitespace only if the corpus moved since the committed sidecars; an unchanged corpus reuses their evidence, and only when every sidecar verifies against its rendition bytes (a missing, corrupt or forged sidecar is not proof of unchanged canon, exit 1). Every consumer's sidecar records the same attestation_text and the same landing_id:

JSON
{
  "algorithm": "sha256",
  "corpus_fingerprint": "6be864c97e6649d004652a3b4b6b63d6a4b4ab7349b3fe68495d7110f5e3f767",
  "corpus_entry_count": 2,
  "rendition_fingerprint": "2af63dcd5a45ca06a7f864a68ad305043d6a8c4ed6274fda0171425e1c84b5ee",
  "committed_ts": "2026-09-27T23:59:01.760782+00:00",
  "attestor": "g0",
  "attestation_text": "corpus delta attested",
  "landing_id": "landing-20260927T235901Z-7a3e2a46"
}

This single attestation is structurally a bundle. One human judgment covers N consumers, the shape AGENTS.md § MAKE LLM STOCHASTIC VIBES INERT names as a vibing signature, and there is no per-consumer repudiation: gz obpi repudiate works at OBPI granularity (ADR-0.35.0 § Consequences, Negative #3). The shared landing_id in every sidecar is what keeps the bundle legible: it tells you which consumers one attestation covered.

Publication contract: journal first, per-file replacement, cleared last

Before the first byte of the first consumer, land takes a surface lock and writes a durable journal, .gzkit/renditions/<surface>/.landing.json, carrying the landing_id, the full intended consumer set, the old and new corpus fingerprints, every target path with its old and new SHA-256, and the attestation. It stages every artifact of every consumer and verifies it before publishing any; a staging failure leaves every committed artifact unchanged.

Publication then replaces one file at a time (atomic per file). It is not an atomic snapshot of the whole set: while a landing is incomplete a reader may observe some consumers on new bytes and others on old. That is the ratified guarantee (brief § Publication Amendment). The landing succeeds only when every target hashes to its recorded new value; then exactly one rendition_landed event is written, and only after that event is durable is the journal cleared. A crash or replace failure keeps the journal, emits no completion event and exits 2:

Text Only
Error: landing landing-20260928T022114Z-0307cbf4 of 'AGENTS.md' is incomplete -- publication stopped: injected: interrupted after the first consumer
The landing journal is retained and no completion event was recorded; readers may observe mixed bytes until the landing completes.
Why forbidden: publication replaces one file at a time (ratified Publication Amendment), so a landing whose every target is not verified against its recorded hash may not claim success.
Next: inspect each consumer with `gz content land AGENTS.md --status landing-20260928T022114Z-0307cbf4`, then resume with `gz content land AGENTS.md`, which reuses the recorded attestation and never rewrites a verified file. Rollback is `git restore --source=<known-good-revision> --staged --worktree -- .gzkit/renditions/AGENTS.md/`.

(Captured in a throwaway project, with the replacement of the second consumer's first file failed by fault injection.)

--status <landing_id>: fingerprints, never mtimes

--status reads the active journal or, after cleanup, the landing's rendition_landed event, and classifies each consumer by the SHA-256 of every artifact's current bytes against the recorded old and new manifests: new (every artifact at its new hash), old (every artifact at its old hash) or indeterminate (anything else, including altered bytes beside a copied good sidecar). It never consults mtimes and never trusts a sidecar's claimed corpus fingerprint. It writes nothing. After the interruption above:

Text Only
$ uv run gz content land AGENTS.md --status landing-20260928T022114Z-0307cbf4
Landing landing-20260928T022114Z-0307cbf4 of AGENTS.md: phase publishing
Manifest: the active landing journal; corpus fingerprint d3ec0bb559f7...
Classified by the SHA-256 of every artifact's current bytes (never mtimes):
  claude: new
  codex: old
  copilot: old

After a hand edit to one consumer of a completed landing:

Text Only
$ uv run gz content land AGENTS.md --status landing-20260928T021919Z-97226600
Landing landing-20260928T021919Z-97226600 of AGENTS.md: phase complete
Manifest: its rendition_landed ledger event; corpus fingerprint d3ec0bb559f7...
Classified by the SHA-256 of every artifact's current bytes (never mtimes):
  claude: new
  codex: indeterminate
    - .gzkit/renditions/AGENTS.md/codex.md: sha256 7f0895e9e471... matches neither its old nor its new manifest entry
  copilot: new
Error: consumer 'codex' of landing landing-20260928T021919Z-97226600 is indeterminate -- its current bytes match neither the landing's old nor its new manifest as a whole.
Why forbidden: only a consumer whose every artifact hashes to one recorded state can be called landed or untouched; mtimes and sidecar claims are no witness.
Next: restore the whole surface's rendition set with `git restore --source=<known-good-revision> --staged --worktree -- .gzkit/renditions/AGENTS.md/` (committed renditions keep no prior version) -- this repairs 'codex' too; a pathspec checkout of that revision leaves newer lineage/retention sidecars behind and reproduces this same mixed set. Then re-run `gz content land AGENTS.md --status landing-20260928T021919Z-97226600`; resume with `gz content land AGENTS.md` only once no consumer is indeterminate.

Resume: no new attestation, no rewritten files

When the surface's journal records an interrupted landing, re-running gz content land <surface> resumes it. Resume reuses the attestation and landing_id recorded when the landing was prepared: it never prompts for or requires --attestor/--attestation-text, and no --force-style override exists. An explicit --attestor/--attestation-text on resume is ignored, and the output says so; the attestation is on the corpus delta, not on the write. Files already at their recorded new hash are never rewritten. Resume refuses (exit 1, nothing written) when the corpus, routing or ownership inputs drifted, or when an artifact matches neither its old nor its new hash (a foreign edit): follow the printed recovery, never an overwrite.

--dry-run over an in-flight landing shows what resume would still publish:

Text Only
$ uv run gz content land AGENTS.md --dry-run
Resume plan (dry run -- nothing written): landing-20260928T022114Z-0307cbf4 (phase publishing)
Attestation: g0 -- recorded in the landing journal
  claude (published):
    rendition  .gzkit/renditions/AGENTS.md/claude.md  new=2af63dcd5a45
    provenance .gzkit/renditions/AGENTS.md/claude.corpus.json  new=2b7fb2d02233
    lineage    .gzkit/renditions/AGENTS.md/claude.lineage.json  new=f8bc52c9780b
  codex (pending):
    rendition  .gzkit/renditions/AGENTS.md/codex.md  new=2af63dcd5a45
    provenance .gzkit/renditions/AGENTS.md/codex.corpus.json  new=2b7fb2d02233
    lineage    .gzkit/renditions/AGENTS.md/codex.lineage.json  new=f8bc52c9780b
  copilot (pending):
    rendition  .gzkit/renditions/AGENTS.md/copilot.md  new=2af63dcd5a45
    provenance .gzkit/renditions/AGENTS.md/copilot.corpus.json  new=2b7fb2d02233
    lineage    .gzkit/renditions/AGENTS.md/copilot.lineage.json  new=f8bc52c9780b
Next: re-run without --dry-run to resume; `gz content land AGENTS.md --status landing-20260928T022114Z-0307cbf4` classifies each consumer first.

Resuming with an explicit, different attestation; it is ignored:

Text Only
$ uv run gz content land AGENTS.md --attestor someone --attestation-text "a different text"
Landed AGENTS.md: landing-20260928T022114Z-0307cbf4
Corpus fingerprint: d3ec0bb559f7... (entries=2)
Attestation: g0 -- recorded in the landing journal (reused on resume)
  claude:
    published rendition  .gzkit/renditions/AGENTS.md/claude.md
    published provenance .gzkit/renditions/AGENTS.md/claude.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/claude.lineage.json
  codex:
    published rendition  .gzkit/renditions/AGENTS.md/codex.md
    published provenance .gzkit/renditions/AGENTS.md/codex.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/codex.lineage.json
  copilot:
    published rendition  .gzkit/renditions/AGENTS.md/copilot.md
    published provenance .gzkit/renditions/AGENTS.md/copilot.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/copilot.lineage.json
Note: --attestor/--attestation-text were ignored: landing landing-20260928T022114Z-0307cbf4 keeps the attestation recorded when it was prepared -- the attestation is on the corpus delta, not on the write.
Resumed landing landing-20260928T022114Z-0307cbf4; already-landed files were not rewritten.
Every sidecar carries this landing_id; rollback is `git restore --source=<known-good-revision> --staged --worktree -- .gzkit/renditions/AGENTS.md/`.
Next: run `uv run gz agent sync control-surfaces` to deliver the landed renditions.

Afterwards every sidecar still records "attestation_text": "corpus delta attested" and "landing_id": "landing-20260928T022114Z-0307cbf4", and the ledger holds exactly one rendition_landed event.

Resume after a kill mid-staging. A process killed after the journal was written but while artifacts were still being staged leaves the journal in phase prepared with no committed artifact changed; --status reports every consumer old. The journal records the target hashes and the attestation. On resume, land regenerates each consumer's bytes from the corpus, checks that they hash to exactly what the journal recorded (a mismatch refuses, exit 1, nothing written), and completes, again with no attestation asked for:

Text Only
$ uv run gz content land AGENTS.md --status landing-20260928T021922Z-424b99bb
Landing landing-20260928T021922Z-424b99bb of AGENTS.md: phase prepared
Manifest: the active landing journal; corpus fingerprint d3ec0bb559f7...
Classified by the SHA-256 of every artifact's current bytes (never mtimes):
  claude: old
  codex: old
  copilot: old
$ uv run gz content land AGENTS.md
Landed AGENTS.md: landing-20260928T021922Z-424b99bb
Corpus fingerprint: d3ec0bb559f7... (entries=2)
Attestation: g0 -- recorded in the landing journal (reused on resume)
  claude:
    published rendition  .gzkit/renditions/AGENTS.md/claude.md
    published provenance .gzkit/renditions/AGENTS.md/claude.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/claude.lineage.json
  codex:
    published rendition  .gzkit/renditions/AGENTS.md/codex.md
    published provenance .gzkit/renditions/AGENTS.md/codex.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/codex.lineage.json
  copilot:
    published rendition  .gzkit/renditions/AGENTS.md/copilot.md
    published provenance .gzkit/renditions/AGENTS.md/copilot.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/copilot.lineage.json
Resumed landing landing-20260928T021922Z-424b99bb; already-landed files were not rewritten.
Every sidecar carries this landing_id; rollback is `git restore --source=<known-good-revision> --staged --worktree -- .gzkit/renditions/AGENTS.md/`.
Next: run `uv run gz agent sync control-surfaces` to deliver the landed renditions.

Retention maps

Every consumer's candidate passes the complete retention gate (§ Retention gate under commit) before anything is written. When a consumer's candidate removes a block of its prior committed rendition, pass a --retention-map for that consumer; repeat the flag once per consumer. A map binds to its consumer by its own surface/consumer fields, not by flag order; two maps for one consumer are a user error (exit 1), and a map naming no routed consumer is a retention refusal (exit 3). Every DROPPED condition id must appear in this invocation's --attestation-text. One refused consumer refuses the whole landing, exit 3, nothing written:

Text Only
$ uv run gz content land AGENTS.md --attestor g0 --attestation-text "corpus delta attested"
Error: the retention gate refused consumer 'claude'; the WHOLE landing is refused. Nothing was written for any consumer.
- [missing-retention-map] Removed block 'doomed rule text.' has no --retention-map accounting for it
Why forbidden: a removed block with an unaccounted condition is an unreviewed meaning loss (ADR-0.35.0 Decision item 10, BI-10), and one refused consumer refuses the bundle.
Next: pass `--retention-map <file>` for 'claude' accounting for every condition of each removed block (KEPT with its candidate span, or DROPPED with a reason), name every DROPPED condition id in THIS invocation's --attestation-text, then re-run `gz content land`.

With a map per consumer and the DROPPED id named in the attestation, the landing publishes each validated map as <consumer>.retention.json alongside the consumer's other artifacts, journaled and hashed with them; a consumer with no removed block has any stale retention sidecar removed in the same publication. Resume reuses the staged retention sidecar and never re-runs the gate against new attestation text.

Text Only
$ uv run gz content land AGENTS.md --attestor g0 --attestation-text "corpus delta attested; C1 drop accepted" --retention-map claude.map.json --retention-map codex.map.json --retention-map copilot.map.json
Landed AGENTS.md: landing-20260928T021924Z-dc633bf7
Corpus fingerprint: d3ec0bb559f7... (entries=2)
Attestation: g0 -- supplied on this invocation
  claude:
    published rendition  .gzkit/renditions/AGENTS.md/claude.md
    published provenance .gzkit/renditions/AGENTS.md/claude.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/claude.lineage.json
    published retention  .gzkit/renditions/AGENTS.md/claude.retention.json
  codex:
    published rendition  .gzkit/renditions/AGENTS.md/codex.md
    published provenance .gzkit/renditions/AGENTS.md/codex.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/codex.lineage.json
    published retention  .gzkit/renditions/AGENTS.md/codex.retention.json
  copilot:
    published rendition  .gzkit/renditions/AGENTS.md/copilot.md
    published provenance .gzkit/renditions/AGENTS.md/copilot.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/copilot.lineage.json
    published retention  .gzkit/renditions/AGENTS.md/copilot.retention.json
Retention (claude):
  C1: DROPPED -- superseded by the seed rule
Retention (codex):
  C1: DROPPED -- superseded by the seed rule
Retention (copilot):
  C1: DROPPED -- superseded by the seed rule
Every sidecar carries this landing_id; rollback is `git restore --source=<known-good-revision> --staged --worktree -- .gzkit/renditions/AGENTS.md/`.
Next: run `uv run gz agent sync control-surfaces` to deliver the landed renditions.

Worked example: dry-run, then land

Captured 2026-09-27 in a throwaway project outside this repository: a surface AGENTS.md routed to three consumers (claude, codex, copilot) whose sidecars were frozen before one corpus entry was appended. Without attestation the new delta is refused (exit 1) and nothing is written:

Text Only
$ uv run gz content land AGENTS.md
Error: --attestor and --attestation-text are required and may not be empty or whitespace: the corpus is a NEW delta, or its evidence is missing -- 'claude': its sidecar is frozen against another corpus fingerprint; 'codex': its sidecar is frozen against another corpus fingerprint; 'copilot': its sidecar is frozen against another corpus fingerprint. Nothing was written for any consumer.
Why forbidden: the corpus attestation attaches to the corpus delta (ADR-0.35.0 Decision 6); a new delta with no attestation would land canon nobody attested.
Next: supply `--attestor <handle> --attestation-text "<the operator's verbatim words>"` and re-run `gz content land`.

The plan (the id shown is minted per invocation; a real landing records its own):

Text Only
$ uv run gz content land AGENTS.md --attestor g0 --attestation-text "corpus delta attested" --dry-run
Landing plan (dry run -- nothing written): landing-20260928T021926Z-0bb8b202
  (the id is minted per invocation; a real landing records its own)
Surface: AGENTS.md
Corpus fingerprint: d3ec0bb559f7... (entries=2)
Attestation: g0 -- supplied on this invocation
Consumers: claude, codex, copilot
  claude (old corpus fingerprint 894845454f8d):
    write     rendition  .gzkit/renditions/AGENTS.md/claude.md  old=74037dddea96 new=2af63dcd5a45
    write     provenance .gzkit/renditions/AGENTS.md/claude.corpus.json  old=b9852900013c new=bd757223ba45
    write     lineage    .gzkit/renditions/AGENTS.md/claude.lineage.json  old=absent new=f8bc52c9780b
  codex (old corpus fingerprint 894845454f8d):
    write     rendition  .gzkit/renditions/AGENTS.md/codex.md  old=74037dddea96 new=2af63dcd5a45
    write     provenance .gzkit/renditions/AGENTS.md/codex.corpus.json  old=b9852900013c new=bd757223ba45
    write     lineage    .gzkit/renditions/AGENTS.md/codex.lineage.json  old=absent new=f8bc52c9780b
  copilot (old corpus fingerprint 894845454f8d):
    write     rendition  .gzkit/renditions/AGENTS.md/copilot.md  old=74037dddea96 new=2af63dcd5a45
    write     provenance .gzkit/renditions/AGENTS.md/copilot.corpus.json  old=b9852900013c new=bd757223ba45
    write     lineage    .gzkit/renditions/AGENTS.md/copilot.lineage.json  old=absent new=f8bc52c9780b
Next: re-run without --dry-run to land AGENTS.md across every consumer above.

The landing, then its status once complete:

Text Only
$ uv run gz content land AGENTS.md --attestor g0 --attestation-text "corpus delta attested"
Landed AGENTS.md: landing-20260928T021927Z-74ceb105
Corpus fingerprint: d3ec0bb559f7... (entries=2)
Attestation: g0 -- supplied on this invocation
  claude:
    published rendition  .gzkit/renditions/AGENTS.md/claude.md
    published provenance .gzkit/renditions/AGENTS.md/claude.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/claude.lineage.json
  codex:
    published rendition  .gzkit/renditions/AGENTS.md/codex.md
    published provenance .gzkit/renditions/AGENTS.md/codex.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/codex.lineage.json
  copilot:
    published rendition  .gzkit/renditions/AGENTS.md/copilot.md
    published provenance .gzkit/renditions/AGENTS.md/copilot.corpus.json
    published lineage    .gzkit/renditions/AGENTS.md/copilot.lineage.json
Every sidecar carries this landing_id; rollback is `git restore --source=<known-good-revision> --staged --worktree -- .gzkit/renditions/AGENTS.md/`.
Next: run `uv run gz agent sync control-surfaces` to deliver the landed renditions.
$ uv run gz content land AGENTS.md --status landing-20260928T021927Z-74ceb105
Landing landing-20260928T021927Z-74ceb105 of AGENTS.md: phase complete
Manifest: its rendition_landed ledger event; corpus fingerprint d3ec0bb559f7...
Classified by the SHA-256 of every artifact's current bytes (never mtimes):
  claude: new
  codex: new
  copilot: new

Like commit, land is not the last step: run uv run gz agent sync control-surfaces to play the landed renditions back to the rendered surfaces.

Rollback

Committed renditions keep no prior version. Each is a single file at .gzkit/renditions/<surface>/<consumer>.md, overwritten by the next landing, so "put it back" means restoring the whole rendition directory from ONE known-good revision:

Bash
# 1. Pick ONE known-good revision and restore the surface's rendition directory
#    from it, so every consumer's rendition, provenance, lineage and retention
#    sidecar come back TOGETHER; never mix files from different revisions.
#    `git restore` -- unlike a pathspec checkout of that revision -- also
#    stages the DELETION of any lineage or retention sidecar a landing added
#    since that revision; a pathspec checkout only overwrites files present in
#    the revision, so a sidecar added afterward is left standing beside the
#    restored (older) rendition -- a mixed set.
git restore --source=<known-good-revision> --staged --worktree -- .gzkit/renditions/<surface>/

# 2. Review the staged restore and deletions before committing.
git status --short .gzkit/renditions/<surface>/

# 3. Check whether that revision matches the CURRENT corpus: restoring old files
#    cannot roll back append-only canon. The freshness gate compares each
#    restored sidecar's corpus_fingerprint with the live corpus. Inside the MX
#    hangar, or whenever the gate is staged in warn mode, drift prints a
#    `WARNING [rendition-freshness ...]` line and the command still EXITS 0 --
#    read the printed output, never only the exit code.
uv run gz validate --rendition-freshness

The landing journal (.landing.json), its staging directory and the lock are not committed artifacts. If the restored sidecars are frozen against an older corpus fingerprint, the renditions are now behind canon: the corpus change still stands, and bringing the renditions level again is a new, attested landing, not a rollback. Retiring the canon itself goes through gz content retire.

Do not delete .landing.json by hand while recovery is still possible. It is the only common record that a landing was in flight across the consumer set, and the tool's own refusals name exactly one governed exception: after a governed decision to ABANDON the landing (for example, a foreign edit or a regeneration mismatch that cannot be resumed), remove the journal named in the refusal's Next: line and start a fresh, attested landing. If a landing was interrupted and recovery has not yet been ruled out, keep the journal, resume with gz content land <surface>, and re-run gz content land <surface> --status <landing_id> to report any remaining drift -- the printed Next: lines name the same one exception.

advise-rendition

Record an advisory information-retained-per-byte verdict for a candidate rendition. This is the advisor-QC stage of the ADR-0.0.37 CMS pipeline (corpus → compress → advisor-QC → operator attest → committed rendition → playback): the agent wielding the gz-advisor-qc skill judges how much information the candidate retains per byte, and records that verdict as an ARB receipt the operator cites at Gate 5.

advise-rendition is deterministic — NO LLM call, NO network I/O. The LLM-as-judge read is the agent's; the tool validates the verdict shape (explanation-before-verdict), writes the arb-step-judge-<hash> ARB receipt, and emits a rendition_advisor_verdict ledger event.

advise-rendition is advisory, never gating (ADR-0.0.39 Evidentiary invariant): ANY score is recorded and the command exits 0 — a low retention score is evidence for the operator, never a fail-closed gate.

Bash
gz content advise-rendition <surface> [--consumer <vendor>] --score <0.0-1.0> --explanation "<reasoning>"
gz content advise-rendition AGENTS.md --consumer root --score 0.94 \
  --explanation "All Mechanical bullets retained; two Promotable bullets combined without information loss."

The command fails closed (non-zero exit, no receipt written) only when the --explanation is empty or whitespace — a structurally malformed verdict. The verdict value itself is never the fail-closed trigger.

Options

Flag Applies To Description
--as <type> import, show, render, edit Content type name (required for these subcommands)
--type <type> list Filter list output to a single registered type
--json list, show Emit JSON to stdout instead of human-readable prose
--write <path> import Write re-rendered canonical form to this path
--vendor <vendor> render, edit Target vendor template for re-rendering (default: claude)
--section <id> remember Target section id or title; normalized to the surface's kebab-case Pillar id (required)
--text <text> remember The entry prose to remember (required)
--tier <tier> remember invariant (verbatim at every setpoint) or compressible (default)
--classification <c> remember Advisory-scorecard class: Mechanical/Promotable/Judgment/Ambiguous (default Ambiguous)
--origin <provenance> remember, retire HOW the capture/retirement arrived, e.g. a GHI or session id (default cli:content-remember/cli:content-retire)
--witness <who> remember WHO vouches for the entry. Recorded provenance, never a gate — capture is never blocked for want of one (GHI #821)
--entry <id> retire Id of the corpus entry to retire (required)
--reason <text> retire Why the entry is superseded; becomes the retraction row's text (required on every tier)
--consumer <vendor> compose, commit, advise-rendition Target vendor consumer (e.g. codex, claude); optional for advise-rendition (surface-wide when omitted)
--candidate <file> compose Path to the candidate rendition file; when omitted, reads piped/redirected stdin with real content (explicit path), or generates from the corpus when stdin is a tty or is empty/whitespace-only (generated path)
--attestor <name> retire, commit, land Defaults to authorship.attestor_handle in .gzkit.json when set (GHI #1036). Operator retiring (retire) or attesting the corpus delta this promotion renders (commit, land); empty fails closed only when the retirement moves invariant-tier liveness (retire) or the corpus moved since the last commit (commit) or since the committed sidecars (land); ignored on a land resume
--attestation-text <text> commit, land Operator's verbatim corpus-attestation token; same conditional requirement as --attestor. Also where a DROPPED retention condition's id must appear (§ Retention gate)
--retention-map <file> commit, land Path to a retention map JSON accounting for every condition of every block the candidate removes from the prior committed rendition. Required only when the candidate drops a prior block; vacuous cases (no prior, byte-identical, blocks only added/reordered, whitespace-only diff) never require it (§ Retention gate). Repeatable for land, once per consumer; each map binds by its own consumer field (§ land)
--dry-run land Print the landing (or resume) plan and write nothing
--status <landing_id> land Classify each consumer of a landing as new, old or indeterminate by content hashes, never mtimes; read-only
--score <float> advise-rendition Information-retained-per-byte verdict value; advisory, never gates (required)
--explanation <text> advise-rendition The advisor's reasoning, recorded before the verdict; empty value fails closed (required)
--quiet, -q global Suppress non-error output
--verbose, -v global Enable verbose output
--debug global Enable debug mode with full tracebacks
--help, -h global Show help and exit

Exit Codes

Code Meaning
0 Success
1 User/config error (unknown type, missing $EDITOR, parse error, validation error, missing file)
2 Usage error (a missing required argument, e.g. land without <surface>), or system/IO error (filesystem unreadable, atomic-replace failed; for land, an incomplete publication whose journal is retained)
3 Policy breach — the retention gate of commit or land refuses a candidate that drops a prior block without an accounting --retention-map, or whose map fails validation (§ Retention gate); writes nothing (for land, for any consumer)

Examples

Bash
# Enumerate registered content types (human-readable table)
uv run gz content list

# Filter to a single type
uv run gz content list --type Rule

# Machine-readable form
uv run gz content list --json

# Inspect a rule file (prose summary)
uv run gz content show .gzkit/rules/tests.md --as Rule

# Machine-readable inspection
uv run gz content show .gzkit/rules/tests.md --as Rule --json

# Render the canonical form of a file to stdout
uv run gz content render AGENTS.md --as AgentContract

# Render against a specific vendor template
uv run gz content render .gzkit/rules/tests.md --as Rule --vendor claude

# Edit a rule with validation guard (invalid edits never land)
EDITOR=vim uv run gz content edit .gzkit/rules/tests.md --as Rule

# Reverse-parse a hand-authored file and write canonical output (OBPI-0.0.34-03)
uv run gz content import AGENTS.md --as AgentContract --write /tmp/agents-canonical.md

# Capture a compressible note into the AGENTS.md corpus (never touches AGENTS.md itself)
uv run gz content remember AGENTS.md --section "Behavior Rules" \
  --text "Prefer stdlib JSONL for append-only stores." --tier compressible

# The append landed in the corpus store, not the rendered surface:
#   .gzkit/corpus/AGENTS.md.jsonl  ← new entry
#   AGENTS.md                       ← byte-unchanged

# Record an advisory info-retained-per-byte verdict for a candidate rendition (advisory, never gating)
uv run gz content advise-rendition AGENTS.md --consumer root --score 0.94 \
  --explanation "All Mechanical bullets retained; two Promotable bullets combined without information loss."

# The verdict is witnessed in the ledger and written as an ARB receipt cited at Gate 5:
grep "rendition_advisor_verdict" .gzkit/ledger.jsonl

Worked example: the retention gate, refused then accepted

Captured 2026-09-25 in a throwaway project outside this repository. The prior committed rendition:

Text Only
# AGENTS.md

- Run `uv run gz check` before every push.

- Never push with `--no-verify`; the pre-push hook is the gate.

The staged candidate drops both blocks, folding them into one line:

Text Only
# AGENTS.md

- Run `uv run gz check` before every push; never bypass the pre-push hook.

Committing without a map names both removed blocks and writes nothing:

Bash Session
$ gz content commit AGENTS.md --consumer root --attestor g0 --attestation-text "compress behavior rules"
Error: the retention gate refused this commit. Nothing was written.
- [missing-retention-map] Removed block '- Run `uv run gz check` before every push.' has no --retention-map accounting for it
- [missing-retention-map] Removed block '- Never push with `--no-verify`; the pre-push hook is the gate.' has no --retention-map accounting for it
Why forbidden: a removed block with an unaccounted condition is an unreviewed meaning loss (ADR-0.35.0 § Decision item 10) — the 2026-09-17 compression dropped 23 binding conditions this way and every check passed (GHI #1090, #1091).
Next: account for every condition of each removed block in a --retention-map (`gz content commit --help`; manpage `content` § commit) — mark each KEPT with its verbatim candidate span or DROPPED with a reason, name every DROPPED condition's id in --attestation-text, then re-run `gz content commit`.
$ echo "exit $?"
exit 3

The retention map (agents.retention.json) accounts for every condition of both removed blocks — C1 and C3 KEPT at candidate spans, C2 DROPPED:

JSON
{
  "surface": "AGENTS.md",
  "consumer": "root",
  "extracted_by": "reviewer-agent",
  "mapped_by": "author-agent",
  "blocks": [
    {
      "removed": "- Run `uv run gz check` before every push.",
      "conditions": [
        {
          "id": "C1",
          "quote": "Run `uv run gz check` before every push.",
          "disposition": "kept",
          "span": "Run `uv run gz check` before every push"
        }
      ],
      "non_binding": []
    },
    {
      "removed": "- Never push with `--no-verify`; the pre-push hook is the gate.",
      "conditions": [
        {
          "id": "C2",
          "quote": "Never push with `--no-verify`;",
          "disposition": "dropped",
          "reason": "the candidate no longer names the --no-verify flag; C3 keeps the intent, the flag itself is dropped"
        },
        {
          "id": "C3",
          "quote": "the pre-push hook is the gate.",
          "disposition": "kept",
          "span": "never bypass the pre-push hook"
        }
      ],
      "non_binding": []
    }
  ]
}

With the map supplied but C2's id absent from --attestation-text, the commit still refuses (dropped-id-not-attested); with C2 named in the attestation text, the same map succeeds:

Bash Session
$ gz content commit AGENTS.md --consumer root --attestor g0 --attestation-text "compress behavior rules" --retention-map agents.retention.json
Error: the retention gate refused this commit. Nothing was written.
- [dropped-id-not-attested] Condition C2 is DROPPED but the id does not appear in attestation text in block '- Never push with `--no-verify`; the pre-push hook is the gate.'
Why forbidden: a removed block with an unaccounted condition is an unreviewed meaning loss (ADR-0.35.0 § Decision item 10) — the 2026-09-17 compression dropped 23 binding conditions this way and every check passed (GHI #1090, #1091).
Next: account for every condition of each removed block in a --retention-map (`gz content commit --help`; manpage `content` § commit) — mark each KEPT with its verbatim candidate span or DROPPED with a reason, name every DROPPED condition's id in --attestation-text, then re-run `gz content commit`.
$ echo "exit $?"
exit 3

$ gz content commit AGENTS.md --consumer root --attestor g0 --attestation-text "compress behavior rules; C2 drop approved" --retention-map agents.retention.json
Committed: .../.gzkit/renditions/AGENTS.md/root.md
Provenance: .../.gzkit/renditions/AGENTS.md/root.corpus.json (corpus_fingerprint=573d9a607eac…, entries=1)
Attested by: g0
Retention:
C1: "Run `uv run gz check` before every push." -> "Run `uv run gz check` before every push"
C2: DROPPED -- the candidate no longer names the --no-verify flag; C3 keeps the intent, the flag itself is dropped
C3: "the pre-push hook is the gate." -> "never bypass the pre-push hook"
Next: `uv run gz agent sync control-surfaces` — this wrote the rendition only; playback is the sole writer of AGENTS.md and its mirrors, so `uv run gz validate --invariant-coherence` stays red until it runs.
$ echo "exit $?"
exit 0

$ ls .gzkit/renditions/AGENTS.md/
root.candidate.md
root.corpus.json
root.md
root.retention.json

Files

Path Role
src/gzkit/content/models/ Canonical Pydantic model definitions (AgentContract, Rule, Skill, …)
src/gzkit/content/templates/ Jinja2 templates per (content type × vendor)
src/gzkit/content/render.py Render pipeline (OBPI-0.0.34-02)
src/gzkit/content/parse.py Reverse-parse pipeline (OBPI-0.0.34-03)
src/gzkit/commands/content/ Operator CLI surface (this OBPI-0.0.34-04)
src/gzkit/content/corpus_store.py Append-only per-surface corpus persistence (remember, OBPI-0.0.37-19)
.gzkit/corpus/<surface>.jsonl Append-only corpus store written by gz content remember
src/gzkit/commands/content/own.py The governed unowned -> corpus-owned transition (own, GHI #974); shares unown.py's journalled transaction
.gzkit/ownership/<surface>.json Section-ownership declaration read and written by own / unown (OBPI-0.35.0-04)
src/gzkit/content/advisor_qc.py Deterministic advisor-QC verdict-record engine (advise-rendition, OBPI-0.0.37-24)
artifacts/receipts/arb-step-judge-<hash>.json Advisor-QC verdict ARB receipt cited at Gate 5
src/gzkit/content/composer.py compose() (explicit path) and generate_candidate() (generated path, OBPI-0.35.0-05)
src/gzkit/content/lineage.py ConsumerLineage/SectionLineage models and the staged/committed lineage path helpers
.gzkit/renditions/<surface>/<consumer>.candidate.lineage.json Staged section-provenance map written by the compose generated path
src/gzkit/content/landing.py The multi-consumer landing transaction: preparation, journaled publication, --status, resume (land, OBPI-0.35.0-07)
.gzkit/renditions/<surface>/.landing.json Active landing journal; present only while a landing is in flight, cleared after its rendition_landed event
  • ADR-0.0.34 — Agent Control Surface Rendering Substrate (docs/design/adr/foundation/ADR-0.0.34-agent-control-surface-rendering-substrate/)
  • Doctrine — docs/governance/agent-control-surface-rendering-substrate.md
  • OBPI-0.0.34-01 — Content model registry generalization
  • OBPI-0.0.34-02 — Rendering pipeline
  • OBPI-0.0.34-03 — Reverse-parse migration tooling
  • OBPI-0.0.34-04 — Authoring CLI (this manpage)
  • OBPI-0.0.34-05 — Light TUI affordances (forthcoming)
  • OBPI-0.0.34-06 — Validation hooks (forthcoming)
  • ADR-0.0.37 — Constitutional Invariant Composition; the remember corpus-capture write path (OBPI-0.0.37-19)
  • ADR-0.35.0 § Decision item 3 — section ownership and the decrease-only ratchet; unown (OBPI-0.35.0-04) raises it, own (GHI #974) lowers it by owning
  • .gzkit/skills/gz-content-remember/SKILL.md — the capture skill that wields gz content remember