Skip to content

v0.9.0: sub-agent transcript capture, the 1:n lifecycle, the record gates and the attestation fix - #638

Open
REPPL wants to merge 47 commits into
mainfrom
feat/sub-agent-transcript-capture
Open

REPPL wants to merge 47 commits into
mainfrom
feat/sub-agent-transcript-capture

Conversation

@REPPL

@REPPL REPPL commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

What

The v0.9.0 release branch: the release-content commit dca2d77 rolls the
changelog derived from the 34 records that reached a terminal folder since
v0.8.0, and the final commit records the two semantic gate receipts naming it.
The merge queue's method must stay merge: a squash would collapse the
two-commit shape the release gate reads.

Breaking, by two records: a refusal in machine-readable mode now answers on
standard output in a self-announcing envelope, and an intent owns one or more
specs, so closing a spec ships its intent only when no open spec remains.

Shipped in this cut:

  • Sub-agent transcript capture: every sub-agent a session spawns leaves the
    same redacted record its main thread does; transcripts already on disk are
    recovered into the repository that owns them; a captured session is handed
    to an agent as one self-contained artefact.
  • The status line and mode: a managed repository shows it is managed and
    whose answer the loop is waiting on, offered at install as its own consent.
  • The intent and spec lifecycle: spec close --remainder mints the follow-on
    spec, and the intent ships when its last spec closes.
  • Two record gates: a record id written in prose must name a record that
    exists, and a commit or pull request that names an issue must declare its
    relation to it; a read-only listing finds the open records history already
    mentions.
  • Identity redaction masks exactly the spans the detector flagged, and a
    reverse-DNS identifier survives capture intact.
  • GHSA-4q78-ccfv-f374: the plugin cache is promoted to the owned copy on PATH
    only when a home-scoped attestation vouches for it, read from a record and
    a home the session can trust.
  • The outbound text check, the deleted-finding refusal, the duplicate-id
    refusal, the dangling PATH link, the absent plugin manifest, and the rest of
    the thirty-one fixes named in the changelog.

Eight findings the merges from main brought into this cut are answered on
their own terms: two resolved on evidence, six deferred with their own
recorded reasons. Three rulings taken this cycle are recorded as decision
records and intent drafts.

Verification

  • make preflight green on every tip today and on the receipts commit;
    make fmt-check clean.
  • Every new behaviour has a test watched fail before the change and pass
    after, each lane reviewed independently before it was committed, and the
    attestation fix reviewed adversarially after merge, with its findings
    closed on this branch.
  • Both semantic gates PROMOTE at full tier against dca2d77; the release gate
    proven locally: go run ./cmd/record-lint --release-gate dca2d779… --require-gate docs-currency-reviewer --require-gate iss35-brief-surface-crosscheck exits 0.
  • The cut derives clean: go run ./cmd/abcd changelog --json reports
    ready: true, next_tag: v0.9.0, impact breaking, no refusals.

Assisted-by: Claude:claude-fable-5-1

REPPL added 30 commits September 9, 2026 07:37
These three records were captured in an earlier session and were left
untracked on main, where nothing substantive is meant to land. They are
committed here unchanged, on the branch, so they stop riding the working
tree of whatever session touches this checkout next.

Not this branch's subject: they concern the launch flow for a managed repo,
a session URL that reached three commit messages and two pull requests, and
the AI-assistance disclosure convention covering commits only.

Assisted-by: Claude:claude-opus-5
The transcript store captures only the top-level session transcript. Every
sub-agent transcript is missed, which on the measured corpus is roughly three
quarters of all recorded bytes: 968 sub-agent files against 68 session files.
The session-end hook reads the single transcript path the harness hands it and
enumerates nothing, and no sub-agent event is registered.

Records the finding, the intent that closes it, its spec, and the lineage
decision the spec turns on. The intent covers three capabilities the product
thinker specified together: capture sub-agent transcripts going forward, ingest
the history already on disk per repository under that repository's own
redaction configuration, and reconstruct any session as one agent-readable
artefact with a telemetry file describing what the work cost.

The ADR settles how lineage is carried. Explicit record fields, not the
composite session id that 176 of the store's 267 records now carry, which
cannot be reached from the identifier of the session that produced it and
never recorded the agent type at all.

Also captures a contradiction found while writing the spec: AGENTS.md says an
ADR ordinal is hand-numbered and needs coordination between checkouts, while
the decide verb mints a collision-proof timestamp id. The stale surface is the
one an agent reads first, and it caused this ADR to be deferred on a risk that
adr-45 had already removed.

Assisted-by: Claude:claude-opus-5
A record could name only the session it belonged to, so a sub-agent transcript
had nowhere to say which session spawned it or what kind of agent produced it.
The only writable identifier was the session id, and hand-capture had been
overloading it as <parent-prefix>--agent-<id>: 176 of the store's 267 records
carry that shape, none of them reachable from the identifier of the session
that produced them, none recording the agent type at all.

Schema version 2 adds seven lineage fields and admits both versions on read.
Capture takes a CaptureMeta rather than a positional list. The lineage scalars
are framed into the raw text and run through the EXISTING two-stage redaction
pass, then split back off, so no field reaches a record without passing the
scanner; a frame that does not survive intact refuses the write.

Supersession makes (session_id, agent_id) the unit. An agent can stop more than
once, and the second stop carries a longer transcript with a different content
hash, so a key on the hash alone writes a second record for one agent: 116 of
999 sub-agent transcripts on this machine show two or more stops, and the store
already carries the same duplication on the main thread. A body whose stored
counterpart is a byte-prefix of it now replaces that record; a shorter
re-arrival is a no-op; divergent bodies stay side by side.

spawn_attribution distinguishes two empties that would otherwise read alike: an
agent spawned by the main thread, and an agent whose lineage is simply unknown.
Capture requires it wherever an agent id is set.

Migration of the existing composite records, the staging sidecar, the hook and
the recovery verbs are later steps and are not in this change.

Assisted-by: Claude:claude-opus-5
Registers SubagentStop and adds the hook behind it, which is the point the
corpus starts accruing the three quarters of recorded work it has been missing.
The hook stages and never captures, and always exits zero: the event is
blocking, so a non-zero exit would stop the sub-agent from finishing.

Staging carries a .stage.json sidecar rather than encoding lineage in the
filename, which would repeat one directory earlier the defect adr-2609090636172016
removes. The sidecar is written before the .raw and torn down if that write
fails, since a .raw without one reads down the legacy path, where a sub-agent's
agent id would be parsed as a session id. Sidecar-less files still drain, so the
raw transcripts already staged on this machine are not stranded. A sidecar that
is present but unreadable is reported rather than falling back to the legacy
reading, which would misattribute it.

The drain takes main-thread entries first and bounds a pass by bytes as well as
count. Staged transcripts are unredacted by design, and capturing sub-agents
multiplies how many exist at once, so a pass that could only count entries would
leave a growing pile of raw text behind.

A sub-agent in an isolated worktree records that worktree as its working
directory and the harness removes it when the agent stops, so resolving the repo
the way session-end does fails exactly where the implementation lanes are. A
session-to-store note gives the hook a second route, and refuses an ambiguous
claim rather than guessing, because the wrong repo means the wrong scanner.

Session capture changes too, deliberately and by decision: session-end now
records its source path, so a session resumed with --continue is stored as one
complete transcript rather than the truncated first half. This is a behaviour
change to existing capture, adopted on the product thinker's call, not a side
effect of the sub-agent work.

Whether the event fires before the transcript is flushed is still unverified.
The mitigations are in (a settle wait outside the staging lock, and a drain-time
re-read that only ever extends a stored prefix) and both now count what they
catch, so the measurement that gates the remaining steps is cheap to run.

Assisted-by: Claude:claude-opus-5
Recorded rather than fixed in place, per the convention that a finding gets an
armed record before it gets a patch.

A staged raw transcript can live indefinitely (critical): staging holds
unredacted text by design and its contract is that a file survives only until
the next session starts, but the drain runs from the session-start hook of the
repository the file belongs to. A repository nobody opens again keeps its raw
transcripts forever. Four such files sit on this machine now, about thirteen
megabytes, the oldest fourteen days old, and the per-repo status verb cannot see
a pile in any other repository.

Ingest would orphan sub-agents whose session resolves perfectly, because it asks
each file's own working directory rather than the session's. That is the right
authority for a main thread and the wrong one for a sub-agent, which records a
worktree the harness has since removed.

Summing reported token usage over-counts it: one response is written as several
lines that each repeat the same usage, so a naive sum multiplies by the block
count. Measured at 1.77x on one transcript and 5.28x across ten, a factor that
varies per session and so cannot be corrected afterwards by a constant.

The shared file lock's hundred-millisecond backoff ceiling admits roughly ten
writers a second, so a burst past forty or fifty simultaneous completions
exceeds the staging timeout and those stages are refused. Pre-existing in the
lock helper, but unreachable until something started staging concurrently.

Assisted-by: Claude:claude-opus-5
…akes a destination

Two recovery verbs over the corpus that already exists.

migrate repairs the records filed under a hand-made composite id. It recovers
the untruncated parent session id from each record's own body, requires the
recovery to begin with the stored prefix, and refuses rather than guessing.
It reports by default and writes only under --apply; the content hash, the
capture stamp and the filename are left alone, because dedup and every existing
reader depend on them.

The spec was wrong about the composite's shape and about what is recoverable.
It gives one form, prefix--agent-id, but 71 of the 176 records carry a workflow
segment in the middle, and splitting left to right refuses every one of them:
splitting from the right is what reads both. It also records agent type and
depth as never held anywhere, when the harness's per-agent metadata still exists
for all 176, so the repair recovers the type, the depth and the parent rather
than writing a record that says its own lineage is unknown. The workflow segment
has no field to land in and is dropped, which the report names.

ingest takes its destination as a required operand and refuses to derive one.
A transcript must be redacted under its own repository's scanner configuration
and never another's, so a destination inferred from the working directory is a
privacy fault waiting to happen rather than a convenience.

Ownership is resolved for the session before the file. A sub-agent in an
isolated worktree records that worktree as its directory and the harness removes
it when the agent stops, so asking the file alone orphans agents whose session
places cleanly: on this machine 19 of one session's 24 sub-agents have a
vanished directory and all 24 resolve through the session. Orphans are ignored
and reported, never guessed, and are adopted only when the destination's own
config names the project.

Records gain adopted_project, so schema 3; parsing stays presence-based and
reads 1, 2 and 3 alike.

Assisted-by: Claude:claude-opus-5
…elemetry

A session's transcripts are only useful as a set, and until now nothing
assembled them. reconstruct takes a session and emits one self-contained
Markdown artefact, readable without the store or the harness's files, plus a
telemetry file describing what the work cost.

Telemetry counts one usage per response, not per line. The harness writes a
response as several content-block lines that each repeat the same usage object,
so summing lines multiplies a response by its block count: on a real session of
55 records, 3711 usage-bearing lines resolve to 1801 responses, an inflation of
2.06x. The factor varies with how many blocks a response happens to have, so a
naive total is not merely imprecise, it is wrong by an amount that changes
between sessions, which defeats the comparison the file exists to support.

The artefact keeps the main thread contiguous and appends each sub-agent, with
spawn and join markers in the thread and a timeline table at the head. The spec
asked for the sub-agent to be nested physically at its spawn point; that is
wrong for asynchronous agents, and the test that proves it shows a delegate's
conclusion rendering ahead of a main-thread turn that ran while it was still
working. On real data one delegate spans 148 turns of its parent's thread.
Nesting survives as data, not as indentation.

Where the record is incomplete the artefact says so rather than reading as
whole: a completeness block reports absent main threads, unattributed agents,
unparseable lines, rejected duplicate records and elided blocks, and every
placement records what placed it, so a consumer can weight a heuristic
differently from a stored fact.

Resolves: iss-2609090723027424

Assisted-by: Claude:claude-opus-5
…ture

Two findings from the branch's security review, both verified by exploit before
being fixed.

The artefact reproduced text blocks raw while fencing every other block type, so
a transcript could emit the document's own headings and markers byte for byte: a
section for an agent that never ran, a turn nobody took, a JOIN marker asserting
a result arrived. The artefact's declared reader is a model being handed the
session as context, and its own guide told that reader a heading it did not
recognise was somebody speaking, which is exactly backwards for a forged heading
that matches a real shape. Anyone who got text into one transcript, from a
fetched page to a hostile sub-agent, could put words in the record's mouth.
Redaction has nothing to say about it: those bytes are content the store
correctly kept.

Text is now fenced like everything else, reusing the dynamic fence this file
already trusts, so there is one escaping mechanism to be right about rather than
a judgement per block type about which content is dangerous. A blockquote prefix
was considered and rejected: prefixing a line that begins the JOIN marker
reproduces the marker exactly, which is the forgery it was meant to close. The
guide now states the rule that shipped.

Separately, migrate framed the lineage scalars before validating them, the
reverse of Capture's order, so a scalar carrying line breaks could re-aim the
frame that redaction splits on and write chosen values into agent_type,
spawn_tool_use_id, lineage_source and spawn_attribution, discarding the real
ones and reporting success. Validation now precedes framing, and frameLineage
itself refuses a scalar containing a line break, so the invariant the store
claims at its boundary is held in core rather than by the front door alone.

Also corrects a comment that was wrong about the harness: exit code 2 is the
only blocking status, so the launcher's non-zero exit on a missing binary is a
visible diagnostic rather than a stall, and silencing it would hide the fact
that transcripts are not being captured. Two reviews independently derived a
critical bug from that comment; the code was right and the comment was not.

Assisted-by: Claude:claude-opus-5
…y do

Staging holds unredacted transcript text on purpose, and its own comment claimed
a staged file survives only until the next session starts. That was false: the
drain runs from the session-start hook of the repository the file belongs to, so
a repository nobody opens again keeps its raw transcripts forever. The store
holds 14.2 MB across three repositories today, the oldest 26 days, and the
per-repo status verb could not see any of it from here.

Sub-agent capture, landed earlier on this branch, turns one staged file per
session into one per sub-agent completion and stages during a session rather
than after it, so this had to be closed before that ships rather than after.

A drain now runs while a session is live, from the prompt hook, with a budget
small enough not to delay a prompt, and ahead of the rules work so an unparseable
rules file cannot switch redaction off. A backlog survey walks every repository
in the store and reports counts and sizes only, never another repository's
identifiers, since that text renders inside a different repository's session.

An age limit buys priority and reporting, not removal. The issue asked for a
staged file to be redacted or deleted at the limit; neither is available.
Redaction is what the drain already does, and a file is old precisely because
nothing ran, not because redaction was tried and failed. Deleting the only copy
is the thing staging exists to prevent. So an overdue entry sorts to the front
of every drain and is named in the notices, and nothing removes it.

A deterministic redaction refusal is now separated from a retryable failure and
quarantined with its reason, rather than re-read and re-refused by every later
drain while filed as awaiting redaction. This changes state, not exposure: those
bytes stay on disk until a human discards them through the one verb that deletes.

Resolves: iss-2609090722466403

Assisted-by: Claude:claude-opus-5
…ntents

The work was filed as one intent covering three capabilities. A conceptual
review argued they were three separable deliveries, and the landing order had
already delivered them as three independent steps that each left the tree green,
which is the tell. The record now says what the code did.

Capture keeps the original intent and its six criteria: a sub-agent's transcript
is stored through the same redact-on-write path as its main thread, at any
depth, fail-closed, idempotent, and reporting a miss rather than swallowing it.
Recovery takes the three about history already on disk, brought into the
repository that owns it under that repository's own redaction configuration.
Reconstruction takes the three about the artefact and its telemetry.

Capture's scope conditions now say plainly what is not known: whether the
completion event fires before the harness has flushed the transcript is
UNVERIFIED. The mitigations shipped and count their own effect, but the residual
rate has not been measured over a corpus, and an intent that claimed otherwise
would be claiming a measurement nobody has taken.

Applied to this machine rather than only tested: 176 records repaired out of
their composite identifiers, 1066 transcripts ingested, the store grown from 267
records to 1104. Thirteen transcripts were refused by fail-closed redaction over
network addresses it could not redact, which is the guard working rather than a
shortfall.

Each close left a fidelity review owed; those receipts stand open.

Resolves: iss-2609081917287384

Assisted-by: Claude:claude-opus-5
Closing each spec reconciled its intent from planned to shipped, and the spec
bodies still linked to the planned folder, so eight links resolved to nothing
and the record gate refused. The close reported success and left the tree
failing its own lint, which is captured separately.

Assisted-by: Claude:claude-opus-5
An abcd worktree held three untracked records and nothing else: an intent draft
about setting up a managed repository's release-rendered site, and two captures,
one about the plugin-provisioned binary lacking a verb its own surface documents
and one about the capture-time scanner over-redacting an ordinary word. Being
untracked, they were in no commit and on no branch, so a worktree prune would
have taken them with it, and 26 worktrees were pruned from this checkout today.

Ownership was established before they were touched rather than assumed: both
live sessions in the repository the worktree was named for were asked, and both
disclaimed the records independently, each observing that all three are about
abcd itself. Nobody claims them and they belong here, so they are committed here.

The draft joins the brief's later-phase index, which is gated on the drafts
folder and refuses a draft it does not list.

Assisted-by: Claude:claude-opus-5
The earlier anti-forgery work fenced the artefact's block CONTENT and left its
block METADATA raw, so the class it set out to close stayed open. A tool call's
name, its identifier, a tool result's identifier and a turn's model name were
formatted straight into the document outside any fence, with no guard but a
helper that substitutes a dash for the empty string. A line break in any of them
emitted lines of the document itself, and the shapes it could emit were an agent
section heading, a turn heading and a join marker: exactly what the earlier fix
was meant to make unforgeable. Its regression test planted the payload only in a
text block, which is why this passed.

The sweep found nine sites, not the four the finding named. Two more are
transcript-controlled: an unknown block's type, and the tool-call identifier the
spawn point is recovered from when placement came from the transcript rather
than the record. The rest are record-derived, where a line break cannot survive
the frontmatter parser but a backtick or a pipe can, and a pipe in the timeline
shifts every later cell so the table reports spans against the wrong agents,
which is a false claim rather than a cosmetic one.

Sites that were already safe were routed through the same cleaner anyway. A
per-site judgement about which values are dangerous is precisely what let this
gap open, and the fix that keeps a judgement keeps the gap.

The cleaner is the repository's existing canonical one-line cleaner rather than
a fourth copy of an escaping rule; the surface layer's own sanitiser does not
fit, because core cannot import it and its semantics are to drop the whole
value, which is right for a record write and destructive for a label.

The guide the artefact hands its reader stated a rule the renderer did not hold.
It now states the one that ships, and it no longer asks the reader to classify
anything: every line of this document begins with words the document chose, and
nothing quoted from the session can begin one.

Resolves: iss-2609091913570877

Assisted-by: Claude:claude-opus-5
Three agent sessions worked autonomously for a day in a managed repository, one
of them filing, planning and implementing an intent end to end without the human
interview. Their run was treated as a field experiment on abcd's gaps: what does
an autonomous operator need that abcd does not give it. This is what they found,
recorded here so it can be assessed rather than lost with their sessions.

Thirteen of these had already been written as properly formed records into a
handoff directory in that repository, over two days, and none had ever reached
this ledger. The channel exists and nothing drains it, which is itself the first
finding about the arrangement.

The synthesis is the most valuable single item and has its own record: the
record verbs are sound, and the failures were all at the edges, where the tool
knows something and does not say it. It holds the enum it is validating against
and names only the offending value; it had the closing commit in hand when it
minted a receipt and did not write it down; it names the file it wants without
saying how to make one. That is a testable claim about where to look, not a
complaint, and it earns a record rather than a footnote.

Two findings were corroborated by more than one session independently, which is
evidence about likelihood rather than possibility: an unknown sub-verb minting a
draft intent from its own name, and required flags being learned from a refusal
rather than from help. Two more were appended to existing records that had
already predicted them rather than filed again.

One record is positive and kept deliberately: every record verb worked from
worktrees across 27 branches, and the ledger's one-file-per-record shape produced
no merge conflict in the whole run. What held is evidence in an experiment, not
filler.

Assisted-by: Claude:claude-opus-5
…correction

The session that hit it checked the filed record and corrected the weight rather
than the content: once the union attribute was in place the decisions log stopped
conflicting entirely, while the changelog went on conflicting on most of the
merges that remained, because union is the wrong remedy for it rather than an
unapplied one. Two conflict classes that look identical at a glance have
different fixes, and a remedy scaffolding only the attribute would close one and
leave the other untouched.

Assisted-by: Claude:claude-opus-5
…corrected

The session that filed, planned and implemented an intent end to end without the
human interview reported nine items at the end of its run. Three were already in
the ledger from the earlier sweep and are left as they are. Five are new. One is
recorded against what was actually measured rather than what was reported.

The corrected one matters most for how these reports are read. The session
believed two captures had been silently lost to an exit status of zero. Tested
on both the published release and current source, an unknown category is refused
with exit status one, the error goes to standard error as JSON, standard output
is empty, and no record is written: verified against the ledger and a clean tree.
Nothing was lost. What the session saw was a machine-readable invocation
producing no machine-readable output, in a pipeline that did not surface the exit
status, for a flag whose accepted values are documented nowhere. Those three
together read as silent loss, and the report was made in good faith. The finding
is recorded as what it is, a shape that makes an operator doubt the store rather
than the flag.

The sharpest new one came from checking the session's own claim against this
repository. It reported writing two invented record ids into a spec and catching
them only on re-reading. Nothing gates that. Attempting the check here showed why
it is harder than it looks: the decisions family carries two filename
conventions, one zero-padded ordinal that differs from the id in its own
frontmatter and one timestamp without the prefix the id begins with, so no cited
id can be mapped to a file by name at all. Any check has to resolve through
frontmatter, which is also why it does not exist yet.

Also recorded: an intent can be planned with no impact and only discovers it at
spec close, in the landing commit, which two sessions hit independently in a day
and both fixed by editing frontmatter because no verb stamps it; the grounds
requirement is documented after the step that enforces it; and cross-session
claim on worktrees and records is coordinated entirely by conversation, measured
at four messages and about fifteen minutes for one session to join work already
in progress.

Assisted-by: Claude:claude-opus-5
…n three

A reporting session corrected the account, and the correction makes the finding
both fairer and worse. The auditors did not invent values at random: each
computed a defensible hash by a stated convention and disclosed which one it had
used, asking the host to substitute its own. The defect is that ingest accepts
provenance the host never issued and cannot tell a conventional self-computed
value from an arbitrary one, so the attestation attests only that some agent
chose something.

This repository has the same condition and acquired it knowingly. Three verdicts
were ingested here today, each carrying hashes its auditor had computed itself
and said so, and the ingest went ahead on the reasoning that the validator would
object if the values were wrong. The validator checks the shape of a hash and
never its value, so it objected to nothing. Three permanent Audit Notes here
carry self-issued provenance, and a managed repository holds three more verdicts
uningested for the same reason, its handover recording the condition so whoever
ingests them does it with open eyes.

Six verdicts across two repositories is enough to call this the normal outcome of
the verb rather than an incident.

Assisted-by: Claude:claude-opus-5
…'s mechanism

The session supplied what its pipeline actually did: it merged standard error
into standard output and parsed the result as JSON, so the exit status was never
read. The refusal reached them, as a well-formed JSON object.

That is a better finding than the one first recorded. On a merged stream a
refusal and a success are both valid JSON, and telling them apart needs either
the exit status the merge discarded or foreknowledge that an error object carries
an error key and a success object does not. Merging streams is an ordinary thing
for a consumer to do, and the machine-readable mode hands it two shapes that
parse identically and mean opposite things.

The record now also states plainly, for any later reader, that nothing was lost:
the captures were refused, no record was written, and the tree was clean, checked
on both the published release and current source.

Assisted-by: Claude:claude-opus-5
The third session's run finished and sent two more, which complete the set.

A verb reveals its requirements one refusal at a time, so resolving one issue
took three calls. Reproduced here: the argument-count refusal names none of the
required flags, and the grounds refusal arrives alone even when the other
required flag is also missing. Each refusal in isolation is well written, and the
grounds one explains itself and confirms nothing was written. The defect is the
sequence, and it falls hardest on an autonomous caller, which pays a round trip
per requirement every time because it carries no memory of the last session's
discoveries.

The other is a positive result, and it is recorded because a stress that finds
nothing is evidence about the tool that is only legible if someone writes it
down. Two adversarial reviews over an autonomously filed, planned and implemented
intent produced eleven findings about the repository under review and none about
abcd. Applying them needed no change to any record shape: a scope condition was
reworded under its existing stamp, one criterion was added, readiness stayed
green, and closing the spec moved both records in one step.

That is the second positive finding from this experiment, and the two of them
bound the negatives. The shapes that carry a claim and the verbs that move a
record between states held under a workload nobody designed them for, a review
loop with no human in it, across 27 branches from separate worktrees, with no
conflict in the ledger. What failed was at the edges.

Assisted-by: Claude:claude-opus-5
Upstream relocated the transcript store while this branch extended it, so the
integration is not mechanical. Upstream's location discipline wins outright: the
store is user-level, self-creating and keyed on the root-commit SHA behind a
single Resolve seam, where this branch had assumed a directory that
`ahoy install` created and `ownedDirsReal` refused when absent. Every place this
branch reached for the old shape now goes through Resolve, and the notice it
printed about a missing store is deleted, because the store cannot be missing.

Our features are ported onto that seam rather than against it: lineage fields and
their validators, supersession on (session_id, agent_id), the staging sidecar,
the live drain, the staged age limit, quarantine, the cross-repository backlog
survey, and the ingest, migrate, reconstruct, drain and discard verbs.

Four things changed in the port and are worth naming. Upstream's ahoy package now
imports history, so this branch's use of ahoy's detector inside ingest became an
import cycle and is replaced by the git primitive underneath it. The SubagentStop
hook shim is rewritten byte-for-byte against the hardened SessionEnd shim, since
ours predated upstream's PATH-ownership check and would have run an unrecorded
binary. Two tests pinning the install precondition are deleted, because upstream
removed the precondition. Two more that provoked a failure by removing the
records directory are rewritten to seal it instead, since the store now recreates
what they deleted.

The two migrations are different things and must not be conflated later:
upstream's moves a corpus between locations, ours repairs composite session ids
inside records.

Assisted-by: Claude:claude-opus-5
Nine findings closed, all of them recorded before they were fixed and each with a
detector watched failing first. They are committed together rather than split
four ways because the generated surface files carry additions from several of them
at once and cannot be separated; the records say which change answers which.

Redaction no longer corrupts a reverse-DNS identifier whose first component is
the account name. The detector suppresses a bare-username match only where it is
an entire component of a dotted run of three or more, so a bundle identifier and a
module path survive while a bare mention, a two-part filename and an email local
part still fire. Before the fix the identifier was masked in the record's body and
in its filename; the residue, that masking still rewrites by whole string rather
than by byte span, is recorded separately because reversing that is a design
decision in the one sanitiser every write path shares.

The refusal that protects a write from a degraded scanner is now asserted, on
seven paths rather than the three named: the sweep listed every unavailability
site, and the decision record, the intent redactor and the lifeboat pack were
unarmed beyond the finding's scope and are armed too. An unasserted guard on a
fail-closed path is indistinguishable from an absent one.

Ingest now verifies that its destination's repository root and its store key name
the same repository, and fails closed when the root's own root commit will not
resolve. The seam exists so that a transcript is never redacted under another
repository's configuration, and it was trusting its caller to supply a pair it
never checked.

A session's sub-agents are reachable from the session identifier: `history list
--session` is the set verb's front door, the human render carries the agent and
its type, and `show` points at the set. The plugin page documents the flag and the
lineage fields, which is what the JSON-only route lacked.

A machine-readable refusal is now on standard output and says what it is, because
a consumer that merges the streams could not tell a refusal from a success without
the exit status the merge discarded. All three closed enumerations now name their
accepted set when they refuse a value, rather than naming only the value.

The findings gate can no longer be cleared by deleting the record it exists to
protect. A deletion is a distinct refusal from an unfixed finding, because the
remedy differs: the record must come back before it can be resolved, and a waiver
is deliberately not consulted, since a deleted record carries no promise to
re-ask.

A record id sitting in two status folders now refuses every read of the ledger.
Folder membership IS the status, so an id in two of them has no status at all.

AGENTS.md no longer tells an author to coordinate an ADR mint that needs no
coordination, and the correction is pinned by a test that mints an id and then
refuses a router contradicting it, so the prose is held to behaviour rather than
to another page of prose.

Resolves: iss-2609100505142469
Resolves: iss-2609091915350221
Resolves: iss-2609091911060345
Resolves: iss-2609091915475296
Resolves: iss-2609100519128005
Resolves: iss-2609091143455568
Resolves: iss-2609100507430423
Resolves: iss-2609090636110810

Assisted-by: Claude:claude-opus-5
…it fixed

The previous commit carried their Resolves trailers and did not move their
records, so the resolution gate refused the range, correctly. The fixes are in
f854118; these are the records catching up with them.

One of the three is stamped breaking rather than fix. Making a machine-readable
refusal self-describing on standard output also means two verbs that render and
then fail now put two documents there where they put one on each stream before. A
consumer that unmarshals the whole of standard output for those two verbs breaks.
The refusal is always last and a test pins that, but the version number should
carry the change rather than a footnote.

Assisted-by: Claude:claude-opus-5
The release gate refuses a cut while a major or critical record captured since
the anchor tag is still open, and the recorded deferral is the only way past. It
is meant to be a judgement, so each of these carries its own reason rather than a
shared formula, and each names why it wants a decision rather than a patch.

They divide into four kinds. Some are capabilities wearing a defect's clothes:
the release flow for a managed artefact that is not a plugin, an errata mechanism
for a durable record, a surface that counts owed reviews, a substrate that records
which session holds what. Some are choices between defensible bargains that the
evidence does not settle: the lock's backoff ceiling, the typed relations the
decomposition discipline names but the format cannot carry, what a hazard guard
should cover beyond shell calls. Some would reverse a recorded decision: making
identity masking span-based rather than whole-string reverses a contract every
write path in abcd shares, in the fail-open direction, and the public banned-names
list cannot be created on a fresh public repository because the visibility fence
is a documented contract with three candidate reconciliations already recorded for
a maintainer to pick between. And one is not a defect at all: the synthesis that
the record verbs are sound and the failures are at the edges, where the tool knows
something and does not say it, which is a lens over the others rather than a
thing to fix.

Two deferrals carry news. The session-URL finding is half closed in this cut, its
CI half gated and its local half blocked on three product decisions about how a
git hook resolves a binary it has no plugin root to find. The unredactable staged
text from a deleted repository no longer sits in the store at all: the owner moved
it out, with a note recording why nothing could ever process it, so what is
deferred is the mechanism and not an exposure.

Assisted-by: Claude:claude-opus-5
… walls

Two findings where abcd refused on a state it had itself produced or could simply
have read as absent.

A link at the PATH target that resolves to nothing made install refuse forever.
The repair existed and was unreachable, because the detector classified every
non-owned link as a foreign occupant to be hand-resolved. The discriminator is
now danglingness rather than provenance: a link resolving to a file is somebody's
install and is still refused untouched, while a link resolving to nothing runs
nothing and is cleared. abcd does not attempt to prove it wrote the link, because
it cannot, and the write-through hazard is closed by removing the link rather than
writing through it.

An unconditional plugin-manifest read refused on any repository that declares no
plugin. The record named one site; the refusal a user actually sees came from a
second, so both are fixed, because fixing the named one alone would have left the
changelog verb dying exactly as before. Absence now comes from the payload tree's
own answer rather than from classifying a read error, so a present-but-unreadable
or malformed manifest still refuses, and a manifest that was declared at the last
release and has gone is still caught as a removal against the baseline rather than
vanishing into an error that yields no verdict.

Resolves: iss-2609100506256636
Resolves: iss-2609100506255436

Assisted-by: Claude:claude-opus-5
…rite it

Three findings, and one repair of my own mistake. The previous commit swept two
files of this work into a commit about dangling install links without the
primitive they call, leaving the branch tip unable to build. That primitive is
here, so the tip builds again; the broken commit stays in history rather than
being rewritten, because it is local and the honest record of what happened.

The record naming which binary the hooks may execute was honoured without anyone
checking who could write it. Four read sites accepted a group-writable or
foreign-owned record, the presence test followed a symlink, and in the proving
test all five hooks executed an attacker-named binary. One primitive beside the
existing guarded read now checks link, regular file, mode and owner before
reading, and the two existing copies were migrated onto it rather than a third
being written. The shims ask the same question with a single file test instead of
parsing a listing, which avoids field-position, locale and access-control-suffix
assumptions, catches the symlink shape, and fails closed when its tools are
absent. This was NOT fixed by the previous release; it was believed to be.

The fidelity-review request now carries provenance the host computed, and the
ingest verifies it rather than checking its shape. The rubric hash covers the
contract this binary enforces, written verbatim into the request so the auditor
echoes the bytes that were hashed; the prompt hash covers the request minus its
own provenance block, which the ingest recomputes. The measured scale was much
worse than the record claimed: thirty-six ingested verdicts, thirteen distinct
rubric values, and two digests appearing under both field names, so the field name
carried no meaning across the corpus. Those thirty-six are deliberately left as
they are, because editing committed audit notes would lay a second fabricated
provenance over the first.

Privacy hygiene no longer fires on the two shapes the conventions mandate, which
is what made it unreadable at a hundred and fifty findings. One half of that
removes a detection and says so in its record rather than hiding in a diff: the
shared-root exemption now covers the subtree, so a third party's name directly
beneath one yields nothing, where the committing user's own name there still
hard-fails.

Resolves: iss-2609091927085132
Resolves: iss-2609100505140261
Resolves: iss-2609100505145554

Assisted-by: Claude:claude-opus-5
…e grew

A window declares what an entry was calibrated for, and three of them no longer
fit. The cause is this cycle's own work: roughly fifty new records from a
three-session field experiment on abcd's gaps, plus the intents, specs and
decision record that carried the sub-agent capture work, all of which the
widening, entailment and detection positions read over.

Each is re-measured at this commit and moved to the smallest ten-thousand
boundary leaving at least one per cent headroom, which is the rule the entries
already state. Nothing about what a position reads has changed, so the kinds are
untouched and only the declarations move.

This is the honest half of a gate that could have been silenced instead. The
alternative the failure offers is to narrow an entry's kinds, which would have
made the number fit by reading less, and none of these entries is reading
anything it should not.

Assisted-by: Claude:claude-opus-5
Four findings deferred at the v0.9.0 cut were deferred because they were
capabilities wearing a defect's clothes: the release flow for a managed artefact
that is not a plugin, errata as a fourth terminal disposition on a durable
record, a count of owed fidelity reviews on the bare status surfaces, and a
claim record for which session holds which worktree. The product thinker
confirmed all four as intents. Each is promoted from its issue so the
back-edges are stamped, and the brief's later-phase index carries the four.

The routing is graded into the calibration note. It is the first run where the
routing question was asked of a class rather than a proposal, and the class had
been drawn by the reason for deferral; that classification had already done the
decomposition's work.

Assisted-by: Claude:claude-fable-5-1
…ry copy

Identity masking rewrote by whole string: every occurrence of the matched text
on a line was replaced, including the ones the detector had deliberately cleared
— a component of a reverse-DNS bundle identifier, a collision inside a longer
word, an occurrence inside a URL span. A line carrying one genuine mention and one
lookalike had the lookalike corrupted too, so the record no longer held the
technical content it existed to hold. The rewrite now masks exactly the byte
spans the detector recorded. Spans are validated against the line before the
secret seal and applied after it, which the seal's byte-length preservation
makes safe; overlapping spans merge into one cluster masked with the widest
member's placeholder; and the line is rebuilt from original bytes, so a
placeholder of any length never shifts a later span.

The cost is accepted with the decision and named in the code: an occurrence the
detector clears on purpose — a login inside a URL, including a credentials URL's
userinfo — is no longer masked by accident, and the second-stage re-scan does
not flag it either, because it is the same detector. The caller's own home path
is outside that residue, because the detector flags every occurrence by the
anchor the literal sweep uses; history, memory and ideate additionally run that
sweep after redaction.

Each proof was watched failing first: the record's own measured case, the
mid-word case, two genuine mentions on one line, offsets across several
length-changing placeholders, the same behaviour for a forge login and a real
name, the seal's length preservation, byte-offset columns under multi-byte
runes, and the fail-open residue pinned so it is known rather than discovered.
A review found the one path that could still reach the whole-string fallback
from a real scan — an identity span overlapping a sealed secret — and it is
closed by validating against the pre-seal bytes, with its own test.

Resolves: iss-2609120446083912

Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-fable-5-1
Three findings deferred at the v0.8.0 cut as needing a human design decision
have that decision now, each taken by the product thinker and each routed the
way the decomposition rule asks, with the hand-run graded into the calibration
note.

The decisions log becomes a folder of individually minted records with a
derived index, and the log's path a symlink to that index for now. The
question first asked was how adoption should propagate the merge-attribute
workaround into a managed repository; the answer came from the same run's
positive finding, that the five one-file-per-record families never conflicted
across twenty-seven merges while the two single appended files conflicted on
nearly every one. The rule is an accepted decision record; the capability is
an intent draft carrying the migration, the index, the adoption path and the
retirement of the decisions-append gate as its acceptance criteria.

An intent owns one or more specs and ships when its last spec closes. A spec
that delivers part of a thought-through intent is closed on its own terms and
a new spec is minted for the remainder; the intent moves only when no open spec
names it. The rule is a decision record and invariant 17 of the brief; its
build is owed, and the issue that asked for it stays open until it lands.

Banned names get both halves: a committed declaration lifts the public
visibility fence so a fresh public repository can create its committed list on
its first commit, and a machine-global private list in the user-level home bans
a name in every repository on the machine, never read by CI. Filed as an intent
draft promoted from its issue.

Refs: iss-2609100507439414
Refs: iss-2609100508570803
Refs: iss-2609100508566552
Refs: iss-2609100506269348

Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-fable-5-1
A spec composed during an autonomous run carried two invented record ids in
its body, and nothing in the gates read a record's prose for the handles it
names; a human reading the document back afterwards was the detector. The
typed cross-references in frontmatter were already resolved, and links were
already checked, and between them sat the thing a session actually writes: a
sentence.

record-lint gains prose_citation_resolves. It reads every record body and every
free-text frontmatter field across the record stores, finds each handle in the
cited-id grammar, folds the spelling a sentence used into the spelling the
resolver keys on, and refuses one that names no record. There is one resolver
in this binary and the rule adds no second. A slug does not stop an id being an
id, so a filename-shaped handle is a citation of the id it starts with, and a
hyphen before the family word does not make it part of a longer handle. An
author marks a numeric placeholder or a forward reference on its own line with
an illustrative or forward-looking marker, and the refusal message says so.

The corpus that predates the gate is carried in a committed baseline keyed on
the id, never on the file, since records move between status folders as a
matter of routine. Each entry declares which class it is and why: a decision
pruned by its successor, an id a predecessor implementation numbered and never
minted here, a record living on an unmerged branch, and one probable defect
carried as suspect for a human to correct. The baseline ratchets in both
directions: a new unresolvable id fails even in a file the baseline names, and
an entry nothing cites any more is reported so the list only shrinks. Sixteen
existing illustrative sites are marked in place rather than licensed globally
by id. Every new behaviour was watched failing first, and the review's findings
on slug-continued handles, free-text frontmatter, the leading hyphen and the
empty-baseline message were each closed with their own test.

Resolves: iss-2609100518527863

Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-fable-5-1
…ng finds the ones it missed

Four fixed issues sat open in a managed repository's ledger because the only
evidence of their fix was commit prose the ledger never reads. A mention that
must declare itself turns that prose into a signal both the gate and the
ledger can read.

RS004 joins the issue-resolution gate: a commit message, or a pull-request
title or body, that names an issue id must carry a declaration line. A
resolves line says the change fixes it, and the existing rule then requires
the record to move in the same diff; a refs line says touched but not fixed
and demands nothing of the ledger. The vocabulary is closed at those two
words, a comma-separated list is admitted on either, and a near-miss is
refused with the two spellings named. The commit half runs where the existing
range check runs; the pull-request form half runs in the attribution workflow,
the one that re-runs when a title or body is edited. Merge commits are exempt
because the forge composes them.

The hint for the backlog that predates the gate is a read-only listing: it
walks the default branch and the ledger, and reports each open record whose id
appears in a commit message without a resolution, ranked by what the commit
did, silent on the commit that filed the record and on a refs declaration. It
lists and never moves a record; the test asserts the ledger is untouched. Run
here it finds seventy-eight rows and no undeclared resolves line, so the
existing rule has held and the backlog is undeclared mentions.

Every proof was watched failing first. The review's findings were closed with
their own tests: a commit body can no longer forge a record boundary in the
walk, since boundaries are checked against the commit graph; a ref that shares
a name with a tracked path walks; each row's exemplar is its strongest
commit; the shell scanners pin a C locale so a stray byte cannot hide a
mention; and the list form on a resolves line is read by both rules, so the
declaration that satisfies one cannot bypass the other.

Resolves: iss-2609100507421759

Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-fable-5-1
…ec closes

Closing a spec shipped its intent in the same step, with no way to separate
them. A session met an intent whose criteria were only half delivered, stopped,
and asked rather than close, which was right; the rule it was missing is now
recorded and built. An intent that has been thought through stands as written.
A spec that delivers part of it is closed on its own terms, a new spec is
minted for the remainder and attached to the same intent, and the intent moves
to shipped on the close after which no open spec names it, with the impact
judgement demanded at that transition and no earlier.

The spec's back-link is the one source of truth for which specs realise an
intent; the intent's own link stays a scalar meaning the spec it was planned
with, and the bidirectional check becomes membership. The set is derived by one
canonical comparison shared by the spec store, the intent corpus and the
record-lint index, because two readers answering "does this intent have an open
spec" differently is the exact hole the review found: a zero-padded back-link
let the store ship an intent the lint saw as still open, and left the second
spec unclosable through any verb. The remainder is minted before any move, so a
failure moves nothing, and a retry reuses an open remainder with that slug
rather than minting a second. A remainder is refused on a shipped intent or a
closed spec. The fidelity audit now names every spec that realised the intent,
and the receipt stays owed once per intent. The release cut refuses a planned
intent only when all its specs are closed, so a release taken mid-delivery is
no longer walled off.

Every behaviour was watched failing first, on a scratch copy, and the review's
findings each closed with their own test. Eighty-six existing specs and
eighty-five linked intents needed no edit. The dispatch pages list an intent's
specs only when more than one realises it, so single-spec pages are unchanged.

Resolves: iss-2609100508566552

Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-fable-5-1
…ing it to PATH

The owned PATH copy was verified only against a binary-meta beside the
artefact, in a directory named solely by CLAUDE_PLUGIN_DATA, so whoever
controlled that variable could have `ahoy install` bless bytes of their
choosing (GHSA-4q78-ccfv-f374, CWE-345). The bootstrap now writes
~/.abcd/cache-attestation (data dir, manifest-authenticated hash, trust,
time; 0600; renamed in whole) only after it has authenticated the cache
against the release manifest, and install promotes a cache only when the
attestation names that directory and the co-located record carries the
attested hash. An unbound cache is refused with a note naming which of
the three checks failed, and install degrades to the pinned symlink as it
does with no cache. An offline bootstrap writes nothing and leaves an
existing attestation untouched. The plugin-root stamp route gets the same
binding; dogfood checkouts are unaffected because the binding is on the
cache, not the plugin-root binary.

Refs: iss-2609012039102770
Assisted-by: Claude:claude-fable-5-1
iss-2609012039102770 resolves with impact fix. adr-2609151706587280
supersedes adr-46, carrying its five decisions forward under the same
numbers and adding the cache attestation as the binding between an
environment-supplied data directory and the provenance install records;
adr-46 is marked superseded. spc-35's "taken from the harness or not at
all" is revised in place, dated. Brief invariant 12 gains the clause. The
decision log records that the maintainer ruled option B on 2026-09-15.

Resolves: iss-2609012039102770
Assisted-by: Claude:claude-fable-5-1
…ue, never the record beside it

The binding check compared the co-located binary-meta with the attestation
and the promotion then re-read that same attacker-writable record to decide
what the artefact must hash to. A writer in the attested directory who swaps
the (artefact, binary-meta) pair for a self-consistent forgery between the
two reads had the forgery promoted — the security review of the first cut
did it in 0.25 s with a flipper goroutine.

cacheBindingProblem now hands back the attestation it accepted, and
installOwnedEntry takes its three verdicts once — hazard, presence, binding —
and acts on those locals alone: `want` is the ATTESTED hash, and nothing under
the data dir is consulted after the binding except the artefact bytes, which
are hashed against it. A test seam (nil in production) occupies the window
deterministically; TestInstallRefusesPairFlippedAfterBinding was watched
failing with the forged pair promoted and no note, and passes with the pair
refused on the checksum, no PATH entry and no provenance written.

Assisted-by: Claude:claude-fable-5-1
…r a name it can predict

The attestation temp was `$HOME/.abcd/.cache-attestation.$$`, written with
`>` and chmod-ed by name; both follow a symlink pre-planted at that
predictable name, so a same-UID writer could have the run write the record's
bytes and mode onto a file of their choosing and then rename the planted link
itself into place as the attestation. mktemp creates a fresh exclusive regular
file under an unpredictable name and the chmod is by the name it returned.

TestBootstrapAttestationTempIgnoresAPlantedSymlink learns the script's pid
before it starts (a wrapper records $$, waits, then execs the script, which
keeps the pid), plants a symlink at the old predictable name, and was watched
failing with the victim's bytes and mode overwritten and the attestation
landing as a symlink; it passes with the victim untouched and a regular 0600
attestation in place.

Assisted-by: Claude:claude-fable-5-1
…write and on the read

CLAUDE_PLUGIN_DATA is an environment value written raw into a line-oriented
record whose Go reader parses last-wins and whose shell reader parses
first-wins: a value carrying a newline injected key=value lines of its own,
and a forged binary_sha256 line after the real one won in Go. The bootstrap
now strips \000-\037\177 from the value before the write — the same class
meta_field strips on read — and readCacheAttestation refuses a record whose
data_dir holds one, since no bootstrap wrote it.

TestBootstrapAttestationStripsControlCharactersFromDataDir (a data dir whose
name carries a newline and a forged record line) was watched failing with a
five-line record and the forged hash parsed; the two new
TestReadCacheAttestationIgnoresMalformed cases were watched failing with the
control-carrying record parsed. All pass.

Assisted-by: Claude:claude-fable-5-1
…ript-capture

# Conflicts:
#	.abcd/development/research/notes/2026-08-15-decomposition-calibration.md
…, mend links

The widening and detection windows no longer fit the corpus, which this cycle
grew by the records of six rulings, the merge of the roles design workstream
and the lifecycle-symmetry intent. Each is re-measured at the merged tip and
moved to the smallest ten-thousand boundary leaving at least one per cent
headroom, the rule the entries already state; the kinds are untouched.

Four prose-citation baseline entries carried ids that lived only on the design
branch; the branch has landed and they resolve, so the ratchet's shrink half
reported them spent and they go. One id written on main as an example of the
minted id shape takes the rule's illustrative marker on its line. Three
decision records and one intent linked issue records by their open path, which
the resolutions of this cycle moved; the links now name the resolved path.

Refs: iss-2609100508566552
Refs: iss-2609100518527863
Refs: iss-2609100507421759
Refs: iss-2609120446083912
Refs: iss-2609120452369809

Assisted-by: Claude:claude-fable-5-1
…ript-capture

# Conflicts:
#	.abcd/development/brief/06-delivery/03-out-of-scope.md
The owned-copy attestation for GHSA-4q78-ccfv-f374, taken into v0.9.0 on the
product thinker's ruling of 2026-09-15 so the advisory publishes with the
release that carries its fix. Five commits from the peer session, reviewed
there for security with three fixes; reviewed again here before the roll.

Refs: iss-2609012039102770
…is cut

The release guard refuses a cut carrying a major finding captured since the
anchor and still open, and the merges of the design workstream and two peer
pull requests brought eight such records into this branch. Each is answered
on its own terms rather than waved through.

Two are resolved on evidence. The roles page rewrite that landed today answers
every complaint the README-and-roles-page finding made, except one phrase a
later merge had silently dropped from the README, which is restored here. The
finding that the conventions file still told authors ADRs keep a hand-numbered
ordinal was fixed three days ago under its twin record, and is closed against
that commit with an internal impact so one correction does not become two
changelog lines.

Six are deferred out loud, each with its own reason on the record. Four of
the design workstream's findings about the fidelity audit, verification rungs
and responsibility for delivered work are settled by decisions the workstream
itself filed as drafts and an open request for comment, so building them now
would answer in code the question the record routes to the product thinker.
The finding that auto-release tags before the gate that can refuse it is
renewed at this anchor with what changed since its first waiver: a second run
falsified the premise the earlier ruling rested on, so the remedy is now the
completion of an accepted ruling, wanted at the top of a cycle rather than at
the end of one. The slug generator's trailing hyphen does not reproduce: every
truncation site already trims the separator, two hundred thousand adversarial
inputs produced no refused slug, and what remains of the record is a
vocabulary ruling.

Resolves: iss-2608290833558547
Resolves: iss-2609111002410678
Refs: iss-2608290820473197
Refs: iss-2608290822140563
Refs: iss-2608290944122400
Refs: iss-2608290956522870
Refs: iss-2609100513521322
Refs: iss-2609120452071388

Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-fable-5-1
…session can trust

An independent security review of the owned-copy attestation found the new
record held a lower bar than the record beside it. The attestation was read
through the guarded read alone, while the path entry it stands before is read
through the declaration read, which additionally refuses a record writable by
group or other or owned by another uid. The attestation chooses the directory
and the hash a release binary is promoted against, so a record anyone else
could write was a record anyone else could aim. It now clears the same
three-part bar, and the comment that claimed it already did is replaced with
what the declaration read adds and why.

The fix's safety argument also assumed the home directory is not something
the environment can choose, and neither reader nor the shell writer pinned
it: a relative home resolved against the checkout, so a committed record
under it would have been read as the user's own. Both readers and the
bootstrap now refuse a home that is unset, not absolute, or inside the
repository being installed, through the one containment helper the data
directory hazard already uses. A refused home is named as the reason, the
install's remedy matches it instead of pointing at network access, and the
bootstrap says why it wrote no record rather than saying nothing. The
containment compare reads the real working directory, never the
environment's word about it, which the existing origin-allowlist test
enforces from the other side.

One finding is captured rather than fixed: a stale attestation permits a
downgrade to the last attested genuine release, bounded to a manifest-verified
binary, since the record carries no freshness. Every behaviour was watched
failing first on a scratch copy; the live home was never touched.

Refs: iss-2609012039102770
Refs: iss-2609151847117977

Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-fable-5-1
The docs-currency pass over the v0.9.0 content commit found a user-facing
verb with no line in the derived release notes: the outbound text check,
which judges a commit message, pull-request body or release note against the
outbound policy and is what CI runs over every message in a pull request's
range. It landed in the change that fixed the session-URL leak into commits,
under records that name the leak and not the verb, and the changelog composes
only from terminal records. The record exists so the release notes can name
what shipped; it is resolved against the commit that shipped it.

Resolves: iss-2609152131205316

Assisted-by: Claude:claude-fable-5-1
…s where the print form refuses

Two currency findings from the semantic pass over the release-content commit.
The README's sample status board follows the adoption step, which leaves the
repository managed, and a managed repository's board carries a fifth line
naming the presence badge, the repository, the branch and the record counts;
the sample now shows it. The mode page said an absent store reads as managed
but not that the print form refuses outside any git checkout, which the
command reference already states; the page now says both.

Refs: iss-168

Assisted-by: Claude:claude-fable-5-1
The dated section is derived from the thirty-four records that reached a
terminal folder since v0.8.0: three shipped intents and thirty-one resolved or
declined issues, two of them breaking. The version is derived from the
records' declared impact, the prose is the release-changelog composer's, and
the binary proved the section cites exactly the cut before writing it. This
is the release-content commit the two semantic gates read; their receipts
follow in the next commit, naming this one.

Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-fable-5-1
Both host-run gates PROMOTE at full tier, which a breaking release requires.
A receipt names the commit its reviewers read and cannot sit in that commit's
tree, so this is the last commit of the release branch and dca2d77 is the
content it attests.

docs-currency-reviewer read the user-facing surface against a binary built
from the first roll of the section: every line verified against the code
with file and line evidence, and every user-facing page changed since v0.8.0
found current. Its three findings were all incomplete rather than false, and
all three were fixed before the section was re-rolled: a shipped verb with no
record and therefore no line, which is now recorded and named; the README's
sample board, which now shows its presence line; and the mode page, which now
says where the print form refuses. The receipt says exactly how the commit it
names differs from the one read.

The brief-surface crosscheck ran its 37 pinned checkers at full tier, five at
a time, and returned 115 discrepancies, all dispositioned. Every one is
design-record prose that ships in no binary; forty-three describe surfaces
this cycle shipped whose brief chapter was not brought up with them, the rest
were already divergent at the last release. Four armings have now returned
246, 137, 109 and 115, a fresh sample each time, so the systematic brief pass
already recorded is the remedy and the receipt says so per finding.

Refs: iss-2609091956001547
Refs: iss-2609152131205316

Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-fable-5-1
@REPPL
REPPL enabled auto-merge September 15, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant