Applies the document contract from
docs/plans/2026-08-20_stack-docs-flatten-and-alignment.md section 3, partially:
`kind` and `status` only. `parent` is deliberately held until the flatten in
section 4 lands, so that 127 documents do not have to be re-pointed by hand
when docs/fleet/NORTH_STAR.yaml moves to docs/NORTH_STAR.yaml.
Scope, measured on origin/next at 63069149:
127 live docs = all *.md under docs/ minus docs/archive/ minus docs/_old_structure/
104 stamped here
19 held operator judgement (plan section 9), worklist in the same PR
3 held the SUPERSEDED TASKS.md stamps, which cite the moving path
1 untouched docs/fleet/FLEET-DOCTRINE.md, already stamped in W1
Kinds applied: 54 guide, 34 record, 9 spec, 6 tracking, 1 projection.
Every row carries a confidence and a one-line rationale in the worklist.
Two collisions with the existing state, both flagged rather than resolved:
1. docs/README.md:150-160 already documents a front-matter convention
(title/type/audience/status/source_of_truth) with its own allowed values.
It is applied to 4 of 127 files. Its `status` vocabulary is
current|draft|deprecated|historical; the new contract's is active|superseded-by.
The key collides. This commit lets the new contract win and rewrites
`status: current` to `status: active` on those 4 files, keeping their other
legacy keys untouched. No code reads any of them: `git grep source_of_truth`
outside docs/ returns nothing. docs/README.md still prescribes the old
convention and is an operator row, so it is not edited here.
2. Two of the plan's 20 operator rows are YAML files, not markdown
(docs/fleet/examples/roster-v2.yaml, docs/openapi-tess.yaml), and the
contract's front-matter form has no defined meaning for a .yaml document.
That gap also applies to docs/fleet/NORTH_STAR.yaml, the source of truth
itself. Raised in the worklist.
A third row from the plan, docs/fleet/north-star.md, no longer exists: W1
renamed it to docs/fleet/FLEET-DOCTRINE.md.
Verification: 104/104 parse with the expected kind and status in front matter;
the check was shown to reject a wrong kind before it was trusted. The diff
removes 4 lines total, all of them `status: current`.
7.9 KiB
kind, status
| kind | status |
|---|---|
| guide | active |
Compaction observer revocation and runtime generations
Status: Current contract reference. Audience: Developer and security reviewer. Evidence: Mutator-gate and framework portability acceptance suites consume this page; live deployment gaps remain explicitly labeled below.
WI-3 connects Claude and Pi compaction/session lifecycle events to the existing authenticated lease-broker state machine. It does not add a second lease store or let runtime hooks assert identity. Each observer inherits the broker-minted session, resolves the current private runtime generation, and sends the existing revoke_lease action over the authenticated Unix socket.
Observer matrix
| Runtime | Lifecycle signal | Action |
|---|---|---|
| Claude / Claudex | PreCompact |
Revoke the current lease before compaction. A non-zero hook result blocks the lifecycle transition. |
| Claude / Claudex | SessionStart with matcher compact |
Revoke again after compacted context starts. |
| Claude / Claudex | SessionStart with matcher resume|clear |
Atomically advance the private generation, then revoke the replacement incarnation. |
| Pi | session_before_compact |
Revoke before compaction; return { cancel: true } if revocation cannot be confirmed. |
| Pi | session_compact then the first context |
Arm and run an independent post-compaction revoke. A failed post observer blocks later tools locally until a retry succeeds. |
| Pi | session_start with reason reload, new, resume, or fork |
Atomically advance the private generation, then revoke the replacement incarnation before reuse. |
The first observer that reaches the broker deletes pending promotion tokens and makes the lease UNVERIFIED. The second compaction observer is deliberate redundancy, not a prerequisite for the first. Claudex receives the same mandatory hooks in its isolated CLAUDE_CONFIG_DIR; hook merging preserves unrelated isolated settings and rejects malformed or symlinked settings fail-closed.
Private generation authority
launch-runtime.py still registers before exec, preserving the kernel-authenticated PID/starttime anchor. It now also creates generation-<broker-session>.state beside the broker socket. The file is owner-only mode 0600 under the broker's mode-0700 directory. Hook descendants read that file instead of relying only on an immutable inherited environment value.
Generation changes use an exclusive file lock, validate owner/type/mode/size, increment monotonically, truncate and write the complete new value, and fsync before contacting the broker. Therefore reload, new-session, resume, and fork events may retain the same PID/starttime while still becoming a new broker incarnation. The higher generation causes the broker to atomically discard prior tokens and lease authority; the replacement generation inherits no VERIFIED lease.
If an observer fires while broker transport is unavailable, revoke-lease.py advances the private generation as a local fence before returning non-zero. Every later all-tools gate reads that higher value. When the broker is reachable again, authentication of that value performs the same old-generation revocation before authorization. Pi also keeps a process-local post-compaction/rollover failure latch that blocks tool calls. An unsafe or unreadable generation file itself makes both lifecycle revocation and tool authorization fail closed.
Threat contract and stopping boundary
BOUNDED RESIDUAL STALE WINDOW
If both pre- and post-compaction observers are missed entirely, no revocation signal exists. During the remaining unexpired lease, within-TTL consequential actions are allowed. Their count and timing are bounded by lease expiry, not by the mutator gate. WI-3 makes no claim that it bounds mutator actions inside this stale interval. The broker's monotonic lease TTL is capped at 300 seconds; after expiry, the next consequential tool is denied with LEASE_EXPIRED.
This is the named D2-v5 T-A residual. It is distinct from an observer that fires but cannot contact the broker: the latter creates a local generation fence and fails closed. It is also distinct from T-C total rot, where the lifecycle observers and the all-tools gate are both absent or replaced. Server-side branch protection, required CI, and independent review remain the irreducible backstop for T-C.
| Condition | Result |
|---|---|
| Either compaction observer succeeds | Existing lease and pending promotion tokens are revoked immediately. |
| Observer runs but broker confirmation fails | Lifecycle transition is denied where supported; local generation fence and runtime latch prevent inherited authority. |
| Both observers are missed, lease unexpired | ALLOWED inside the bounded residual stale window. No within-window mutator bound is claimed. |
| Both observers are missed, lease expired | DENIED by monotonic TTL expiry. |
| Generation advances on reload/new/resume/fork | Prior incarnation revoked; replacement starts UNVERIFIED. |
| Lifecycle observers and all-tools gate both fail or are removed | T-C total-hook-miss residual; protected-branch controls remain required. |
T-C server-side branch-protection posture
The required posture is that main is push-blocked and PR-only-merge is MANDATORY, regardless
of client-gate state. The client-side gate narrows the exposure window only; it is not the T-C
guarantee. The server-side protected-branch configuration is the irreducible guarantee for protected
repository actions. Status-check enforcement and approval enforcement are RECOMMENDED.
Current-vs-required gap (recorded, not enacted)
The current empirical configuration is recorded here without re-probing or mutating live branch
protection. enable_push=False (push-block present), so the mandatory push-block/PR-only-merge core
holds. require_approvals=0 (approvals not enforced), enable_status_check=False (status checks not
enforced), and block_on_official_review=False (official review not enforced). Those recommended
merge-quality controls are the current gap; changing them is a separate, owner-gated operations
decision and is not enacted by this documentation change.
The permanent T12b/T30 acceptance case prints both required outcomes: dual-hook miss within TTL is ALLOWED, and the same lease after TTL is DENIED. Separate real-socket tests prove each Claude observer and same-PID generation rollover; Pi lifecycle tests exercise pre/post observers, all four replacement reasons, and local failure closure.