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¶
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.
--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.
--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.
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).
--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().
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.
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.
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:
$ 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:
$ 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).
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):
$ 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):
$ 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):
$ 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.jsonrestored 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 (TestCommittedDeclarationLoadsCleanlyloads the committed declaration against the committed surface on everygz check). Neitherunownnorowncan act on an edited map:ownclears the floor relation over its successor map and is then refused because the map on disk is not the one itsfloor_event_idwitnesses — the edit never had a witness. - Grown section. With
project-identitygrown by one line,ownrefused until the corpus carried every content line and named each uncovered line with thegz content remembercapture for it; after four captures,ownlanded —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.
$ 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.jsonreproduced the refusal above, andgit 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: nogz contentverb 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:
$ 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:
$ 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_unownedrow. 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-recoveryand 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_digestbefore 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.
$ 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:
$ 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.
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:
$ 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):
$ 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).
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):
$ 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.
$ 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.
# 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.
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:
{
"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:
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-textcame 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.
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):
$ 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:
{
"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:
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:
$ 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:
$ 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:
$ 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:
$ 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:
$ 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:
$ 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.
$ 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:
$ 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):
$ 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:
$ 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:
# 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.
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¶
# 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:
# 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:
Committing without a map names both removed blocks and writes nothing:
$ 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:
{
"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:
$ 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 |
Related¶
- 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
remembercorpus-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 wieldsgz content remember