918 requirements: 918 complete, 0 incomplete, 0 findings [OK] REQ-ACCESS-RETIRED-VERB-REFUSAL required: [impl, unit] stages: -doc +impl +unit -int A RETIRED SUBCOMMAND OF `spt endpoint access` REFUSES BY NAME RATHER THAN FALLING THROUGH ITS OWN OPTIONAL POSITIONAL AND ANSWERING PLAUSIBLY. `access` takes an optional endpoint id positionally, so once `list` and `rules` stopped being subcommands (retired when the roster views replaced them) clap handed those tokens to the POSITIONAL, and the view reported `no access entities ruled for 'list'` at exit 0 -- a well-formed, confident report about an entity nobody has ever created. Found by flynn in the v0.49.0 field verify (releases#67). THE DEFECT IS NOT A MISSING ERROR MESSAGE, IT IS AN ANSWER: the sentence is TRUE of any name nobody has ruled, so it is indistinguishable from the real result for a real endpoint, and an operator or agent still carrying the old form reads it as a report and stops looking. The same failure shape as a stale carried-forward instruction -- the surface looks diligent and is wrong. MEASURED BEFORE THE FIX, and the measurement widened the filing: `rules` falls through IDENTICALLY, so the population is the retired-token FAMILY and not the one token the issue named. The node tier is NOT affected and that was checked rather than assumed: `spt node access` declares no positional at all, so clap refuses an extra argument natively. THE REFUSAL FAILS OPEN ONTO A REAL ENDPOINT. This guard is added to a path that previously accepted EVERY positional, so it is a narrowing, and a narrowing has an unruled arm: an endpoint genuinely named `list` must stay viewable. The known-target check therefore runs FIRST and wins -- a fix that made a real target unreachable would be a new defect wearing the old one's repair. Known means the access store rules it or a local perch carries the id; the store is already loaded at that point, so the check costs no read. The token match is case-folded, because an operator who typed the old verb in any casing typed the old verb. Gate: impl -- the retired-token table with its replacement guidance, the case-folded lookup, the refusal composer naming token then retirement then the working form, and the guard placed AFTER the known-target check inside the view; unit -- a decision table whose expectations are WRITTEN OUT AS LITERALS: each retired token refuses with a nonzero exit and names its replacement, an ordinary unruled endpoint id still gets the honest roster sentence (the silence the fix must not swallow), a real endpoint that happens to BE named `list` is viewed rather than refused, the casing variants refuse alike, and every token in the table is a token the current grammar does NOT accept as a subcommand -- walked against the real parser, so a token that comes BACK as a verb cannot sit here refusing itself. [OK] REQ-ACCESS-VERDICT-REASON required: [impl, unit] stages: -doc +impl +unit -int The endpoint access gate reports WHY a pass passed, not merely THAT it passed: the gate's verdict carries, on its allow arm, the reason the chain admitted the interaction — the same-node short-circuit, the reply exemption, an entry whose subject NAMED the peer (a proven sender endpoint or the origin node), an entry that matched only by SUBNET WILDCARD (an entry, but one that never named this peer), or a posture pass (an endpoint / node / captured-subnet mode, or the implicit-open bottom of the chain). The reason is computed AT THE GATE, in the one place that holds the whole chain, and travels with the decision. The alternative — a downstream consumer re-deriving it — means RE-OPENING the access store after the gate has closed it, which is both a second read of a value that may already have moved (the stale-snapshot class) and a second entry point into the security chain; the trust warning is a consumer of the decision, never a second decider. Refusals stay BARE: a denial's diagnostic tier is already printed where it is refused, and no enforcement caller does anything with a deny but drop it, so giving Deny a payload would force every refusal site to destructure a field it never reads. Enforcement sites therefore gate on a refusal PREDICATE, which is what keeps the ripple across them rename-shaped rather than a scatter of partial matches. Same-node and reply are carried as reasons in their own right precisely so that a consumer can never warn on them: same-node is inside the node's own trust unit, and a reply is traffic the receiving agent itself invited. Gate: impl — the pass-reason type, the payload on the allow arm, the tier-to-reason classification, and the predicate helpers, with every production enforcement site converted to the predicate; unit — one row per pass arm produced by the REAL gate rather than by calling the classifier directly, each row arranged so that only its own arm can produce the pass (the named-entry rows sit on a closed endpoint, the wildcard row's peer is a subnet member no rule names, and the same closed endpoint is asserted to refuse a non-member), plus the helper split: the refusal predicate is true for exactly the refusal, its complement holds for every pass reason, and no pass reason is readable from a denial. [OK] REQ-ACL-ACCESS-REFRESH-ER-ONLY required: [doc, impl, unit] stages: +doc +impl +unit -int spt api access-refresh stops refusing and becomes real, gated to the engine room, updating ONLY the node's captured subnet-level fallbacks (ADR-0052 decision 6; the W2 refusal REQ-ACL-ACCESS-REFRESH-VERB was minted precisely so this wave changes one behavior rather than adding a surface and its gate). The refresh is the CONSENTED half of advisory gossip: a subnet-mode change reaches a member as a notification, the engine room is briefed with the exact new posture, and a human decides whether this node adopts it — which is why the verb writes the captured fallbacks and never the node's own rules, and why no remote actor can invoke it. Its authentication is the same shared engine-room function empower uses. The W2 refusal test is repinned to the new contract in this same change, not left asserting a behavior the wave removed. Gate: doc — ADR-0052 decision 6 and the CONTEXT.md capture-refresh sentence; impl — the real refresh behind the shared engine-room gate, writing captured subnet fallbacks only; unit — an engine-room caller refreshes and the chain's bottom tier changes, a non-engine-room caller is refused, the node's own rules are untouched, and the repinned W2 test asserts the new contract. [OK] REQ-ACL-ACCESS-REFRESH-VERB required: [doc, impl, unit] stages: +doc +impl +unit -int `spt api access-refresh` is MINTED THIS WAVE BUT REFUSES — the verb exists, parses and is documented, and its refusal names why: the capture-refresh is engine-room-only, and engine-room enforcement (ADR-0052) does not land until W3. Minting the refusing verb now is deliberate: it fixes the contract adapters and the engine-room brief will be built against, and it makes the wave that implements enforcement a change to ONE behavior rather than a new surface plus its gate. A refusal that merely says 'unknown command' would invite an adapter to route around it. When it does light up it updates ONLY the node's captured subnet-level fallbacks — never the node's own rules, which are the operator's, not the subnet's. Gate: doc — the CONTEXT.md capture-refresh sentence naming the verb and its engine-room-only gate; impl — the verb, parsing, and a clear refusal naming the W3 dependency; unit — invoking it refuses with the engine-room diagnostic, changes no stored state, and is not reachable as an unknown-command fallthrough. [OK] REQ-ACL-DISCOVER-DEFAULT-ON required: [doc, impl, unit, int] stages: +doc +impl +unit +int DISCOVER IS ON BY DEFAULT: a `closed` posture at ANY mode tier -- endpoint mode, node mode, or the join-time-captured subnet mode -- no longer implies a DISCOVER deny, and the ONLY thing that closes DISCOVER is a deny that NAMES it: an access rule row, or a `per_surface` mode entry (engine-room-set at the node tier). Operator-ruled 2026-08-17 (releases#180/#181): being findable is what makes a knock -- the ask to be admitted -- possible at all, so a node that closed its posture to say 'do not talk to me' had also silently said 'and you may not ask', which was never the choice being made; the messaging-first majority pays for a discovery decision they never took. THE DEFAULT LIVES IN THE SURFACE VOCABULARY, NOT BESIDE THE CHAIN: the `surface::TABLE` row grows a `default_on` discriminant (single-source, the shape `attributable` already established) and exactly ONE method reads it -- `Modes::for_surface_governing`, which for a default-on surface resolves `per_surface` ALONE (exact key then case-fold, the releases#66 precedence rule VERBATIM) and declines the `self.all` blanket fallback. `Modes::for_surface` is untouched, so every reader that asks 'what posture is set here' keeps its present meaning; only the readers that DECIDE change. ALL THREE MODE TIERS AND `effective_posture` ROUTE THROUGH IT: the posture reading is deliberately the chain's own tiers in the chain's own order, so leaving it on the blanket would put two accounts of one fact out of agreement -- the exact failure that method exists to prevent -- and the `may_grant_node_subject` consequence is CONSISTENT rather than a hole, since post-change a node-subject DISCOVER allow admits nothing the default already does not. `MatchedTier::SurfaceDefaultOpen` is minted: a pass by surface default and a pass by chain bottom are different facts, needed by the ER/DISCOVER conjunction notice and the trust warning's pass-reason, and cheap only at mint. WHAT DOES NOT MOVE: ADR-0053's degrade (a store that cannot be read still refuses DISCOVER outright -- the default is a posture carve-out, never a licence to advertise policy we cannot read), REQ-ER-NOT-ADVERTISED's pre-chain engine-room carve-out (posture beats whitelist), and the redemption path, which is DISCOVER-free by design and node-tier-routed. Gate: doc -- the CONTEXT.md mode-tier entry stating the carve-out as ratified vocabulary, naming the one off-switch and the three things that do not move; impl -- the table discriminant, its single table-driven reader, the governing resolver, the three tiers, `effective_posture`, and the new tier; unit -- a blanket-closed node admits DISCOVER and STILL refuses MSG in one cell, an explicit DISCOVER-naming mode closes it at each of the three tiers SEPARATELY, a DISCOVER-naming deny ROW still denies, the case-folded key is honoured ON THE NEW PATH with exact-match precedence intact, the chain and `effective_posture` agree in both directions, the default-open pass is distinguishable from the implicit-open bottom, the reader is proven a function OF THE TABLE against a fixture the shipped table cannot express, and a degraded store refuses DISCOVER beside a readable blanket-closed one that admits it. [OK] REQ-ACL-DISCOVER-GATE required: [doc, impl, unit] stages: +doc +impl +unit -int DISCOVER is a real gated surface: registry resolve/advertise and the resources blurb are filtered per viewer, so discovery leaks nothing a viewer could not reach. ADR-0009 NAMED this leg — 'discovery (resource advertisement) is gated by the first two gates' — and CONTEXT.md's resource-advertisement entry promises that an endpoint whose access rules exclude the viewer's node by naming DISCOVER in a deny never appears in that view; a legacy grant plus blanket-closed posture does not imply that exclusion because DISCOVER is default-on. Only the VISIBILITY half was ever built; the access half has been an unbuilt promise since M4. Un-gated discovery on a shared subnet is an inventory of every agent on every member node, including the ones an operator deliberately walled off — and a resources blurb is a yellow-pages description written to attract exactly the collaboration the access rules refuse. Gate: doc — the CONTEXT.md resource-advertisement entry (the promise this closes) and the control-surface entry minting the id; impl — the DISCOVER check on the registry resolve/advertise path plus per-viewer filtering of the resources blurb; unit — a viewer denied by a DISCOVER-naming rule sees neither the endpoint row nor its blurb, while a viewer allowed DISCOVER sees both, legacy blanket closure alone remains disclosed, and the filter is per-viewer rather than global. [OK] REQ-ACL-ER-DISCOVER-CONJUNCTION-NOTICE required: [doc, impl, unit, int] stages: +doc +impl +unit +int AN ACCEPTED RULE THAT CANNOT CURRENTLY TAKE EFFECT SAYS SO: an endpoint-scoped access mutation naming the ENGINE ROOM is accepted and, when its `DISCOVER` half cannot presently disclose anything, the accept site prints a LOUD CONJUNCTION NOTICE naming the engine room's advertisement whitelist as the other lever and its current posture. Disclosure of the engine room is an AND across two records: `DiscoverGate` checks the ER advertisement filter BEFORE the access chain (REQ-ER-NOT-ADVERTISED, ratified and unchanged by this requirement), so a rule accepted against an empty whitelist discloses NOTHING while reading as policy in force. Field case: ENLYZEAM's sole `DISCOVER` allow was ER-scoped `any-of SPT_DEV` with an empty whitelist, and every SPT_DEV peer saw only its own rows (hertz RCA 2026-08-05). THE RULED SHAPE (doyle, releases#163) IS ACCEPT-AND-TELL, and both halves of it are load-bearing: do NOT refuse the mutation -- the rule IS the conjunction's legitimate chain half and refusing it would make the only expressible half unexpressible -- and do NOT route it into the ER whitelist, because that record is the human/engine-room seat's authority and an AGENT-INVOCABLE VERB MUST NOT MUTATE IT. Kin doctrine: refused-rather-than-quietly-ignored (#5); the defect is DECLARE-SITE VS ENFORCE-SITE SILENCE, and acceptance is not the defect. WHICH HALF IS MISSING IS NAMED, not merely that one is: the whitelist names nobody; the whitelist does not name the machine this rule admits (checkable EXACTLY for a node subject, because the whitelist is node-keyed -- an advertisement lands on a peer NODE and every endpoint there reads it -- and deliberately NOT guessed for a subnet wildcard, which this site cannot enumerate into node keys, NOR for a sender-endpoint subject, which names no node at all: resolving that id to a machine would be a guess wearing a lookup, since the directory answers where this node last SAW that endpoint and not where the grant will be exercised from, so a notice built on it would name the wrong machine with full confidence. Both fall-throughs are the same non-guess doctrine and both are PINNED BY A CELL, because an unstated deliberate silence is indistinguishable from an unhandled arm to the next reader (doyle, #146 review); or the whitelist would disclose and the posture withholds ahead of it. GAPS ARE REPORTED WHITELIST-FIRST THOUGH THE GATE CHECKS POSTURE FIRST: posture is a state that changes the moment a controller attaches, the whitelist is the durable lever the operator must actually edit, and naming a transient blocker while a permanent one sits underneath sends them to fix the wrong thing. ONLY AN ALLOW IS CLASSIFIED -- a deny's `DISCOVER` half takes effect the moment it lands, so a notice there would teach that a refusal is conditional when it is not -- and a REMOVAL never reaches the notice by an explicit op guard rather than by the decision it carries, since a removal restates the tuple with the WIDENING polarity the gate wanted and not the rule's own. The record is read only when the target IS the engine room, so an ordinary rule mutation gains no file read. Gate: doc -- the CONTEXT.md entry stating the conjunction, the accept-and-tell shape, the whitelist-first order and what must not happen; impl -- the pure classifier over (target-is-ER, rule, whitelist, posture), the notice composer, and the accept-site call after the acceptance line; unit -- each gap arm classified apart, an ALLOW that the whitelist DOES name plus an online posture yields NO notice (the silence is earned, not merely unimplemented), a deny and a removal are silent, a non-ER target is silent, and the notice text names the whitelist as the other lever and the posture as its current state. [OK] REQ-ACL-FAIL-CLOSED required: [doc, impl, unit] stages: +doc +impl +unit -int A corrupt or unreadable access store degrades CLOSED, loudly (ADR-0053 — an ADR-level flip of ADR-0009's deliberate fail-open ruling). ADR-0009 justified fail-open explicitly: the whitelist was 'a same-subnet convenience boundary layered inside subnet membership, not the system's outer wall', because every member node was the one user's own machine. The SHARED SUBNET ruling breaks that premise — member nodes now belong to different human operators and the access layer gates agents — and a boundary that evaporates when its store file corrupts is not a boundary an operator can reason about. So a store that CANNOT BE READ refuses unlisted-subject traffic on every surface rather than admitting it, and says so on the refusing node naming the store path (silence plus refusal reads as network failure and burns diagnosis time). TWO THINGS SURVIVE THE DEGRADE: the stateful-firewall reply exemption (correlation state is a separate file keyed on the endpoint's own recent outbound — a degraded node keeps its own conversations alive) and the same-node loopback Allow. THE SPLIT THAT MAKES THIS SHIPPABLE: an ABSENT store file is NOT a degrade. It is the unconfigured baseline — every fleet node today has no access.json, and reading absence as closed would refuse all unsolicited cross-node traffic fleet-wide on upgrade. The two cases split on std::io::ErrorKind::NotFound and nothing else: never-created reads as an empty store whose chain bottoms out open; any other io error or a parse failure is a degrade. Absent-is-open keeps an unconfigured fleet reachable, unreadable-is-closed makes the boundary real. Consequence accepted per ADR-0053: a personal fleet with zero configured rules now blocks unsolicited cross-node traffic if its store file CORRUPTS — the failure is loud, local, and repairable, and the alternative (silent admission on a shared subnet) is the worse lie. Existing fail-open tests are repinned to the new contract as part of this diff (a product contract change, not a test fixup). Gate: doc — ADR-0053 and the CONTEXT.md control-surface-modes entry carrying the degrade rule; impl — AccessStore::load_checked/load_checked_from returning AccessDegraded (path + cause) with the NotFound split, and the daemon gate's refuse-plus-diagnostic arm ordered AFTER the reply exemption; unit — the degrade split (absent is baseline, corrupt and unreadable both degrade, diagnostic names the path) and the gate-level proof that the same call is allowed before corruption, refused on every surface during it, still allowed as a reply throughout, and allowed again once the store is repaired without a restart. [OK] REQ-ACL-FORK-WITHOUT-DISCOVER-CONSEQUENCE required: [doc, impl, unit, int] stages: +doc +impl +unit +int A `FORK` GRANT WHOSE SUBJECT CANNOT RESOLVE THE ENDPOINT SAYS SO AT WRITE TIME -- a stated consequence, never a refusal: the rule lands exactly as typed and the operator is told that as written the subject cannot resolve the endpoint it may now fork, so the attempt fails later as an unresolvable subject. Forking a remote endpoint takes TWO surfaces and only one is obvious: `FORK` authorizes the operation, `DISCOVER` is what lets the grantee resolve the row at all. Field origin (releases#76, from the releases#29 cross-node rung): node B granted node A `FORK` alone, A could never fork, and granting the pair fixed it -- diagnosed empirically, not by reading. The operator-facing defect was never that the pair is required; it is that the requirement was INVISIBLE, since the write succeeds and reads back as typed. THE CONDITION IS THE CHAIN'S OWN `DISCOVER` VERDICT FOR THE SUBJECT, NEVER RULE-ROW PRESENCE, and the re-derivation matters because #76's own filed premise stopped being true inside this milestone: the filing reasoned that per-surface isolation closes every unlisted surface so a FORK-only grant DENIES DISCOVER, but read against the code a FORK rule simply does not COVER DISCOVER (`AccessRule::covers`), so DISCOVER fell THROUGH to the mode tiers and it was the blanket-closed posture there that refused it -- exactly what REQ-ACL-DISCOVER-DEFAULT-ON removed. A row-scan condition would therefore now be wrong in BOTH directions: silent on the explicit deny that still bites, loud on the blanket close that no longer does. ONE VERDICT, TWO CONSUMERS: the same predicate answers this write-time consequence and REQ-KNOCK-PRESCRIBES-FORK-PAIR, asked of one function rather than described twice -- this file already records what a second copy of an approval classifier cost. SUBJECT SHAPES ARE ANSWERED HONESTLY OR NOT AT ALL: a NODE subject is exact; a SUBNET WILDCARD is one rule with a verdict per machine, so the refused members are NAMED (with the count this node can SEE stated beside them, because a roster is this node's view and not the subnet's truth); a SENDER-ENDPOINT subject is deliberately NOT answered, since `FORK` carries no proven sender and such a rule matches no traffic at all -- its real defect is a different sentence owned by the attributability surface, and adding 'it could not resolve either' would rank a second problem above the first. A blanket rule (empty surface list) covers `DISCOVER` by definition and is never a gap; a DENY is never a gap. Gate: doc -- the CONTEXT.md entry stating the pair, the verdict-not-rows condition and the re-derivation that makes it necessary; impl -- the shared verdict function over a caller-held roster/subnet snapshot, the gap classifier, the consequence line, and the accept-site call; unit -- a FORK-only allow to a DISCOVER-denied node states the consequence, the same grant to a node that CAN resolve is silent (the earned silence), a FORK+DISCOVER grant is silent, a blanket grant is silent, a deny is silent, a subnet subject names the refused members apart from the seen count, and a sender-endpoint subject is silent by ruling rather than by accident. [OK] REQ-ACL-GRANT-NODES-POLICY required: [doc, impl, unit] stages: +doc +impl +unit +int ENDPOINTS_CAN_GRANT_NODES is a node-tier policy toggle, settable only via the engine-room, gating whether an ordinary endpoint may write a NODE-SUBJECT entry into its own whitelist — the widened grant a non-attributable-surface approval produces. Explicitly set it is true or false; UNSET DERIVES PER-DECISION from the effective posture of the REQUESTED SURFACE at the target node: open => endpoints may self-approve (an allow entry on an open surface punches no hole), closed => engine-room only (a node-subject allow would punch a hole in a closed posture). The gate applies in BOTH POLARITIES — adding a node allow and removing a node deny are the same widening act — and it is enforced at the STORE MUTATION SEAM rather than in any one caller, so neither the CLI nor the knock-answer path can bypass it. It is distinct from the resolution chain's node-scope TIER: per-node chain entries are engine-room-owned always, policy or no policy; this toggle governs only the SUBJECT side of self-sovereign whitelists. The engine-room is not bound by it — its bring-up gate is the authorizing ceremony. Name caveat accepted at mint: 'grant nodes' means node-SUBJECT grants, not node-tier rules. Gate: doc — the CONTEXT.md policy entry; impl — the toggle, the unset-derivation from the requested surface's effective posture, and the both-polarity check at the mutation seam; unit — explicit true and false both honored, unset derives open-permits and closed-refuses from the requested surface, removing a node deny is gated exactly as adding a node allow, and neither the CLI nor the knock-answer path can reach a widening write without passing the seam. [OK] REQ-ACL-INTRA-NODE-SELF required: [doc, impl, unit, int] stages: +doc +impl +unit +int INTRA-NODE GOVERNANCE IS A SELF-REFERENTIAL NODE SUBJECT AT THE EXISTING TIERS, NOT A NEW TIER. Operator-rephrased 2026-08-22, superseding in full the v1 reading (a slot-3.5 rules tier with a mode twin at 6.5): #211 adds NO tier, NO Subject kind and NO schema change. A rule whose subject is THIS NODE'S OWN id governs traffic authored on this node, and it does so through machinery that already shipped. Two effects, both operator-confirmed: a NODE-SCOPE own-node rule governs intra-node actions for every endpoint hosted here, and a PER-ENDPOINT own-node rule governs incoming actions from other same-node endpoints. The third -- endpoint-targeted rules evaluate before node-targeted -- is the chain's existing order and nothing moves. THE MECHANISM AND THE CLAIM ARE DIFFERENT THINGS, AND ONLY THE MECHANISM WAS ALREADY PINNED. REQ-ACL-LOCAL-ORIGIN-TIERS stopped local traffic short-circuiting above `AccessStore::decide` and its unit cell `a_rule_naming_the_local_origin_still_governs_it` already proves a node-scope DENY naming the local origin refuses a local MSG, and that the RULE tier answered rather than a mode. THAT CELL CANNOT PROVE THIS REQUIREMENT, AND ITS OWN HELPER IS WHY: `req_local` constructs `Origin::LocalNode` DIRECTLY and carries an ARBITRARY origin string ("beef"), never `local_node_hex()`. So it proves the chain governs a request ALREADY classified local whose origin string a rule HAPPENS to name. This requirement's claim is a JOIN of two facts that sit on opposite sides of that helper: the local authoring path SUPPLIES THIS NODE'S OWN HEX as `origin_node` (`gate::admit_local_delivery` passes `local_node_hex()`), and therefore the string an operator actually WRITES -- their own node's key hex -- is the one that matches. MEASURED AT 2b855595: no test in the tree, unit or integration, constructs `Subject::Node { node: }`; `er_inbound_local.rs` writes a `SubnetWildcard` DENY, which is the hole-punch negative and a different claim. The join has no unit form for the same reason REQ-ER-INBOUND-LOCK-ALL-PATHS's did: the authoring verb decides what reaches the chain, and only the real binary runs the authoring verb. RULES-ONLY, AND THE MODES KEEP ABSTAINING FOR LOCAL ORIGIN exactly as REQ-ACL-LOCAL-ORIGIN-TIERS built them -- the hole-punch property (a closed node/endpoint/subnet mode never severs the box's own agents) holds by construction, and #211's original default-deny example is expressed as a node-scope own-node DENY on MSG rather than as a posture. PRECEDENCE ABOVE THE CHAIN IS UNCHANGED AND RESTATED RATHER THAN IMPLIED: the engine room's inbound lock is step 0 and a self-referential rule never touches it (ADR-0052); a correlated local reply is admitted with no rule of its own (REQ-SEC-1), own-node deny included; and KNOCK is its own surface, so an own-node MSG deny leaves knocking to KNOCK's own evaluation. AN OWN-NODE SUBJECT CAN NEVER MATCH A REMOTE ORIGIN -- hex inequality, and the classification puts any request whose origin equals the own hex on the LocalNode arm before the chain runs. LOCAL GOVERNANCE IS IMPOSSIBLE BY CONSTRUCTION ON A NODE WITH NO IDENTITY, and that is a design property rather than a hole: `admit_local_delivery` passes `local_node_hex().unwrap_or_default()`, so an unprovisioned node hands in an EMPTY origin_node -- it still classifies LocalNode and still flows, but no rule can name a hex that does not exist. An unprovisioned node is exactly the state a first-run operator is in, so the property is asserted rather than left to be reported as a bug later. Gate: int -- the JOIN, driven through the real binary with no daemon: an ungoverned local send ADMITTED as the control on the same store, then refused under a NODE-SCOPE rule whose subject is the hex the binary itself minted, then refused again under a PER-ENDPOINT rule with the node-scope rule CLEARED so it cannot be the thing answering; each rule read back OUT OF THE STORE by match count rather than echoed from the argument, and each refusal asserted to have spooled NO row, so a slow delivery cannot pass as a refusal. Kin REQ-ACL-LOCAL-ORIGIN-TIERS (the axis this runs on, and whose synthetic-origin helper is why this evidence is owed), REQ-ACL-SUBJECT-CHAIN (the tier order it uses unchanged), REQ-ER-INBOUND-LOCK-ALL-PATHS (the admission site the local path crosses), REQ-SEC-1 (the reply exemption that outranks an own-node deny). [OK] REQ-ACL-LOCAL-ORIGIN-TIERS required: [impl, unit] stages: -doc +impl +unit -int WHERE AN INVOCATION CAME FROM IS DATA THE CHAIN REASONS ABOUT, NOT A SHORT-CIRCUIT BEFORE IT. Until releases#209 the chain's step 1 was `if local, return Allow(SameNode)`, so locally-authored traffic never reached `AccessStore::decide` at all. That was invisible while local traffic was ungated, and it is exactly what would have foreclosed the operator's intra-node governance surface (releases#211) — which under the ruled shape is NOT a new tier between the per-endpoint entries and the node tier, but SELF-REFERENTIAL Node subjects at the EXISTING tiers (v1's slot-3.5 reading withdrawn in full, operator-rephrased 2026-08-22). A tier the local path short-circuits past is not a tier. So `AccessRequest` carries an `Origin` SET AT EXACTLY ONE CONSTRUCTION SITE — the chain's own — and never re-derived per call site, because a discriminant with two derivations is one that will eventually disagree with itself. THE LOCAL VARIANT CARRIES THE ORIGIN ENDPOINT ID, not merely the fact of locality: same-node governance discriminates by SENDER at the EXISTING tier-1 `SenderEndpoint` subject, so the id has to reach `decide` as part of the origin rather than be re-derived at each call site. Dropping it at construction would cost a second plumbing pass through this very seam — and releases#215 measures precisely what re-derivation costs when it is left to call sites: three feeders hand this one parameter three different value shapes, and a tier-1 rule silently cannot match on one of them. It is carried from the first commit even though nothing consumes it yet, and it is carried in the VARIANT rather than beside it so that constructing a local origin without deciding what to put there is not expressible. Its provenance differs from the wire's and the type says so: a remote sender endpoint is DAEMON-STAMPED and session-proven, while a local one is SESSION-DETECTED in the authoring process — never the caller-supplied `from`, which an explicit `--from` owns and which would admit a forged subject (KNOWN-HAZARDS 7.5, the same reasoning that keeps `from` out of tier 1). The same-node allow is DEMOTED to the chain BOTTOM, where it always semantically belonged: nothing named this traffic, so it is the same-node operation it always was. THE FIX MUST WIDEN NOTHING, and two tiers would have widened it silently if left alone. (1) THE MODE TIERS ABSTAIN FOR A LOCAL ORIGIN. A mode is a blanket posture about who may reach this node FROM OUTSIDE it; `closed` has never in this codebase's history been asked about traffic authored on this machine, because such traffic never reached the function. Letting the modes answer now would convert every `closed` posture in the field into 'and also stop the agents on this box talking to each other' — the hole-punch failure arriving through a door nobody opened deliberately. (2) A LOCAL ORIGIN CARRIES NO SUBNETS. This node is a member of its own subnets, so handing them in would let a subnet-wildcard DENY — written to keep some peer group out — begin refusing locally-authored traffic the moment that traffic reached the chain. Only rules that NAME the local origin explicitly speak about it; everything else falls to the same-node bottom, so today's local behaviour is preserved BY CONSTRUCTION rather than by inspection. A STORE DEGRADE MUST NOT CLOSE THE NODE IN ON ITSELF: ADR-0053's fail-closed refusal protects the node from the network, and applied to local traffic it would stop the agents here talking to each other every time `access.json` was mid-write, so a local origin takes the chain bottom on a degrade. The engine room is unaffected by that arm — its lock is step 0, above all of this, and its own posture read is independently fail-closed. A NODE WITH NO IDENTITY reads as local: nothing can be proven remote either, since a wire arrival carries a handshake-proven hex by construction (REQ-HAZARD-WAN-ORIGIN-AUTH), so an empty origin can only have come from a local authoring path — and that is also the fail-open direction for the same-node hole-punch. THESE SEMANTICS ARE NOT DECIDED HERE. This requirement owes #211 a reachable slot and a carried subject, nothing more: the ORIGIN AXIS and the local sender it carries. NO NEW TIER AND NO MODE TWIN ARE OWED — the v1 shape (a slot 3.5 with a mode twin 6.5, whose twin would refuse local traffic rather than abstain) is WITHDRAWN IN FULL, operator-rephrased 2026-08-22: #211's intra-node governance is SELF-REFERENTIAL Node subjects at the EXISTING tiers, rules-only, with modes-only-abstain staying exactly as this requirement built it. The ER lock stays at step 0 regardless, because same-node governance is a rules surface and never a replacement for the ratified engine-room semantics. Gate: impl — the `Origin` axis on `AccessRequest` set at one site and carrying the local sender id, the mode-tier abstention, the empty local subnets, the demoted same-node bottom, and the local degrade arm (the v1 slot markers are NOT part of this requirement's evidence and are removed with the withdrawal above — a marker describing an unbuilt tier certifies a design, not an implementation); unit — the hole-punch negatives with a CONCRETE adversary rule in the store (a subnet-scoped DENY present, and a closed node mode present, and a local send still admitted under each), plus a remote send under the same store still refused, which is what proves the abstention did not simply disable the tiers rather than scope them. Kin REQ-ACL-SUBJECT-CHAIN (the tier order this inserts into), REQ-ER-INBOUND-LOCK-ALL-PATHS (the lane that needed the axis), REQ-SEC-1. [OK] REQ-ACL-LOCAL-SUBJECT-ONE-SHAPE required: [impl, unit] stages: -doc +impl +unit -int EVERY LOCAL FEEDER OF THE ADMISSION GATE HANDS IT A PROVEN ENDPOINT ID OR NOTHING — ONE MEANING, ONE SPELLING (releases#215): `gate::admit_local_delivery`'s `sender` is a SUBJECT (`Subject::SenderEndpoint` matches it by equality), and its three local feeders disagreed on what they put there — `spt send` passed the session-proven bare id, `spt ring` passed `resolve_from(--from)` (where an EXPLICIT `--from` BEATS session detection), and `notify` passed `NotifRow.from_id`, the COMPOSITE `{issuer}@{node_hex}`. A tier-1 rule an operator wrote for endpoint `ling`, that the CLI accepted and the view renders, therefore covered two of three local delivery verbs and COULD NEVER MATCH ON NOTIFY: it failed in the SILENT direction. `access_check_with_sender`'s own doc already forbade exactly this (NEVER pass `WanMessage.from` here -- an explicit `--from` beats session detection in `resolve_from`, so it would admit a forged subject), as does REQ-ACL-SUBJECT-CHAIN's ratified title — the code contradicted three landed documents. NOT FILED AS A SECURITY CLAIM, deliberately: REQ-ER-INBOUND-LOCK-ALL-PATHS already publishes same-user local code as outside what any spt gate holds, so a forged-subject framing would overclaim against a boundary we published ourselves. THE REMEDY IS ONE DERIVATION SITE, NOT THREE CORRECTIONS: `session_proven_sender` is threaded IN from the CLI (`ring::ring` gains a `sender_proven` parameter in `cmd_send`'s shape) rather than re-derived inside each verb, and `cmd_notify`'s INLINE RE-SPELLING of `resolve_from`'s body — the drift mechanism that let two sites diverge with neither looking wrong on its face — is replaced by a call to it. NOTIFY PASSES `None` AND TIER 1 ABSTAINS (doyle ruling 6): the chain falls to the node tier, byte-for-byte the documented behaviour for a sender who proved nothing. Splitting the composite at `@` is REFUSED as a fix — deriving a subject from a DISPLAY string is the same defect one layer down, and `from_id` legitimately carries the composite FOR RENDERING, which a fix blaming it would break. THE SMALLER PROMISE IS RECORDED, NOT SWALLOWED: a tier-1 rule about `ling` still does not MATCH on notify — it abstains BY DESIGN — and carrying a proven-sender field on the row so it could match is tracked as a separate EVAL item, never as delivered here. THE AUTHORING-TIME REFUSAL GAINS THE OTHER HALF OF THE SAME CONFUSION (doyle ruling 5): `build_access_rule` already refuses a sender-endpoint rule on a surface carrying no proven sender (`ACCESS_DEAD_SUBJECT`, REQ-ACL-SURFACE-ATTRIBUTABILITY) — it now also refuses a COMPOSITE typed into the bare-id slot (`--endpoint ling@`), which can never equal a proven bare id, in that arm's existing vocabulary. A rule that can never match must refuse at authoring time whichever half of the confusion produced it. SIDE EFFECT PINNED, NOT LEFT INCIDENTAL (doyle ruling 9): `admit_local_delivery` records the stateful-firewall reply window with `note_outbound(sender, …)` — the SAME string — so a caller-chosen id could open a reply window it never earned; one argument feeds both, so normalizing the feeders closes it BY CONSTRUCTION and a unit asserts that rather than leaving it to be re-derived. That closure is reported on the ticket carrying its own limit verbatim: its consumers were read, never measured. Gate: impl — the threaded `sender_proven` parameter + the notify `None` + the `cmd_notify` call to `resolve_from` + the composite-subject refusal; unit — the three feeders asserted TOGETHER (send and ring hand a proven bare id or None; ring's gate subject is UNCHANGED by an explicit `--from`, the arm that fails on the pre-fix spelling; notify hands None so tier 1 abstains rather than judging a display string), the composite-into-bare-slot refusal named and distinct from `ACCESS_DEAD_SUBJECT`'s surface arm, and the `note_outbound` window opening only for a proven subject. [OK] REQ-ACL-LOCKED-POSTURE required: [doc, impl, unit] stages: +doc +impl +unit -int The **locked** claim is one predicate, and it asks the CHAIN. `ACCESS_LOCKED` (the `revoke` arm) and the access view's `locked` field are two renderings of a single fact — this endpoint refuses ALL unsolicited remote inbound — and both take that fact from ONE predicate, `AccessStore::is_locked(endpoint)`. It is CHAIN-SCOPED by necessity, not by preference: the superseded `EndpointAcl::is_locked` was record-scoped and therefore structurally unable to answer, because an endpoint that names no posture of its own inherits one from the node mode and from captured subnet modes, neither of which a record can see; it counted node-subject allows (`allowed_nodes().is_empty()`) and never read a mode at all, so a `revoke` on a default-open endpoint told the operator their endpoint now refuses all remote inbound while the chain admitted everyone, and `--json` said `locked: true` about a store the human view correctly rendered as open in the same breath (releases#206). LOCKED MEANS, for every surface in the set: the effective posture is closed AND no ALLOW rule at endpoint-or-node scope covers it. BOTH halves are load-bearing — posture alone re-mints the same false sentence in mirror image, since rules outrank modes (tiers 1-5 above 6-8) and a closed endpoint carrying one allow row still admits somebody. Only ALLOW rows are holes (a deny row is the posture agreeing with itself), an allow qualified by origin class is still a hole because it still admits somebody, and the hole-scan reads surface coverage through the chain's OWN matching machinery rather than re-deriving what covers means — a second reading of rule semantics is precisely how the two renderings drifted apart. THE SET IS DERIVED, never enumerated: every non-default-on row of the surface table, so a later wave's surface joins the claim by the sole act of existing, and the derivation is proven against a FIXTURE table whose default-on row is not DISCOVER (with `TABLE` a const, a reader hardcoded to all-but-DISCOVER agrees with every shipped row and no test over `TABLE` can tell the two apart). DISCOVER — the one default-on row — is deliberately OUTSIDE the predicate: a blanket `closed` never GOVERNS it (releases#180), so a literal reading of the sentence would be false for every blanket-closed endpoint in the fleet and the line could never truthfully fire at all. That fact is carried BESIDE the sentence by `discover_still_open_line`, required ADJACENT IN THE SAME EMISSION wherever the locked claim prints, sibling-arm precedent: the sentence alone is the overclaim, sentence-plus-caveat is the honest composite, and the `ACCESS_LOCKED` wording itself is unchanged under this shape. The JSON view exposes the same facts from the same predicate and the same composer data, so the machine and human accounts cannot disagree. Scope of the claim is REMOTE — same-node reach and the reply exemption lie outside what the sentence asserts. Separately but in the same lane: `AccessStore::revoke` prunes a record left carrying neither rules nor modes, as `apply_mutation` already does on a Remove, because `is_vacant`'s own doc names a vacant record as the shape that must not persist — it read as restricted through the old predicate. Gate: doc — the CONTEXT.md locked-claim entry; impl — the one chain-scoped predicate, the derived surface set, the chain-machinery hole-scan, the adjacent discover caveat at every emission site, and the vacant prune in revoke; unit — a revoked default-open endpoint is not locked, a node-wide close locks an endpoint that names no posture, a closed endpoint with an allow hole is not locked while a deny row does not unlock, an origin-qualified allow still counts as a hole, a node-scope allow holes an endpoint's lock, DISCOVER stays outside the claim while a fully closed endpoint still reads locked, the surface set derives from a fixture table carrying an invented row, and revoking the last rule leaves no vacant record. [OK] REQ-ACL-MODE-ADVISORY-GOSSIP required: [doc, impl, unit] stages: +doc +impl +unit -int A subnet-mode change gossips ADVISORILY — it produces a notification and nothing else. An existing member's EFFECTIVE posture never changes remotely: the captured mode (REQ-ACL-SUBNET-MODE-CAPTURE) is immutable except through the node's own refresh (REQ-ACL-ACCESS-REFRESH-VERB), so no remote party can reach into a member node and re-posture its gate. This is the difference between a subnet owner ADVISING members of a policy change and COMMANDING their enforcement — on a shared subnet whose members are different humans, only the former is defensible, and a producer that quietly applied would be a remote write to security material. Gate: doc — the CONTEXT.md control-surface-modes advisory-gossip sentence; impl — the notif producer on subnet-mode change, with NO apply path; unit — a mode-change gossip raises the notification and leaves the receiving node's captured mode and its decisions byte-identical. [OK] REQ-ACL-MUTATION-HUSK-REFUSAL required: [impl, unit] stages: -doc +impl +unit -int ADR-0053'S FAIL-CLOSED HAS A WRITE SIDE, AND IT IS ENFORCED AT THE SAVE SEAM RATHER THAN AT THE MUTATION CALL SITES. `load_checked` split NotFound from corrupt and the gate refused a husk correctly -- but the store is READ-MODIFY-WRITE at every path that edits it, and `AccessStore::load` is `load_checked_from(..).unwrap_or_default()`, so a degrade was swallowed into an EMPTY document. The consequence inverted the guarantee (releases#57): the gate refused unsolicited traffic, and then the first `spt endpoint access` command an operator ran to investigate the refusal saved that empty document over the damaged file -- permanent rule loss, and a posture that flipped from degraded-closed to open-with-one-rule. The boundary held exactly until someone touched the CLI, and the command most likely to be run is the one an operator reaches for BECAUSE the gate is refusing. THE SWALLOWED DEGRADE IS THEREFORE CARRIED ON THE DOCUMENT (`StoreOrigin::Degraded`, `#[serde(skip)]`, never part of the record) AND `save_to` REFUSES IT. This placement is the requirement, not an implementation detail: converting the nine production mutation call sites to `load_checked` closes the population that exists today and leaves the next read-modify-write path free to re-open the class -- the same shape of defect as REQ-SEC-REPLY-EXEMPTION-SEND-LEG's per-caller write, which shipped inert because a rule satisfied by remembering to is satisfied until someone forgets. Every write of this document funnels through `save_to`, so a refusal there binds call sites that do not exist yet, and an audit is not what holds it. The call sites ARE converted as well, for the diagnostic: a refusal at the load names the store before anything is attempted, which reads better than one raised by a save the operator already believes succeeded. FRESH IS NOT DEGRADED: a document constructed in memory -- the minted baseline, a test fixture -- stands in for nothing on disk and stays writable, so mint-on-first-absence (the split that keeps an unconfigured fleet reachable) is untouched. The refusal names the store path per ADR-0053, names the cause, and names the way out, which is real rather than rhetorical: a store the operator MOVES ASIDE is re-minted empty by the next command, so starting from empty stays available as a deliberate, loud choice -- which is precisely what silently rewriting it from empty was not. Gate: impl -- `StoreOrigin` on `AccessStore` set by the loaders (`load_from` marks the swallowed degrade, `load_checked_from` marks a parsed store `Loaded`), the `save_to` refusal in the single voice of `husk_write_refusal`, and the nine production mutation sites loading checked (cli.rs `endpoint access` allow/revoke/open + subnet-mint capture + endpoint purge, api/engineroom.rs `enforce`/node-mode/node-surface-mode, daemon pairhost.rs join-time capture); unit -- a decision table over the three origins whose expectations are WRITTEN OUT rather than derived from the code under test, asserting on-disk bytes and not merely the returned Result: a husk-derived document refuses the write and the damaged file survives BYTE-FOR-BYTE, a loaded document's read-modify-write preserves the rules it did not touch, a fresh document writes, and the refusal names the store path. [OK] REQ-ACL-NODE-MODE-SET required: [doc, impl, unit] stages: +doc +impl +unit -int The node's control-surface modes are settable through the engine room and nowhere else (ADR-0052 decisions 1 and 3, CONTEXT.md 'control-surface modes' — the node level of the three). Modes are exactly what a confused or adversarial agent would loosen, so the mutation surface must be the one place an agent cannot reach without passing a human-held TOTP; every other candidate — a plain CLI verb, an elevation-gated verb, a config file the daemon reads — is reachable by something running as the user. Subnet-scope mode authority is separate and rides empower (REQ-SUBNET-EMPOWER-VERB); this requirement is the node's own posture, which needs no empowerment because the bring-up gate already proved subnet membership. Gate: doc — the CONTEXT.md control-surface-modes entry naming the engine room as the node-level setter; impl — node-scope mode writes reachable only through an engine-room-authenticated path; unit — an engine-room caller sets a node mode, every other caller is refused, and the resolution chain reads the written mode at its node tier. [OK] REQ-ACL-NODE-VIEW required: [doc, impl, unit] stages: +doc +impl +unit -int `spt daemon access` is the node-tier roster: the entities the node-scope rules name (node and subnet-wildcard subjects — a node-scope sender-endpoint rule cannot exist by schema), this machine's own mode with its per-surface exceptions, and the captured subnet modes — the tier every hosted endpoint falls through to, in the same item grammar as the per-endpoint view so the two scopes read as one system. There is NO `spt subnet access`: a subnet is a subject tier and a mode source, never a rule-holding target — its mode facts surface on `spt subnet status` (REQ-SUBNET-STATUS-MODES). Gate: doc — the CONTEXT.md access-entity entry; impl — the node roster builder and the daemon verb; unit — the node roster's entities, mode-exception summary, and explicit empty rendering. [OK] REQ-ACL-ORIGIN-QUALIFIER required: [doc, impl, unit] stages: +doc +impl +unit -int An access rule may carry an ORIGIN QUALIFIER (any|user|agent) restricting it to invocations of that origin class, so a rule can say 'the humans on node A, not its agents' and let that node's agents fall through to the mode. TWO TYPES, NOT ONE (doyle-ruled design): the RULE side is a three-valued qualifier defaulting to `any` for N-1 records; the REQUEST side is a TOTAL two-valued class (user|agent) with no unknown arm, because `any` is a rule-side value that no classifier ever emits — a single three-valued type would let a classifier return `any`, which is meaningless. Classification is ambient and ceremony-free, performed by the SENDING daemon over the existing local-origin predicate (interactive terminal with no perch/broker ancestry => user; agent-session ancestry => agent) and carried to the receiving gate as a daemon-stamped sibling of the sender-endpoint stamp — the same shape of fact, so the same seam, never a parallel path. ABSENT OR UNCLASSIFIABLE MAPS TO agent AT THE RECEIVING BOUNDARY: the restrictive class, so a `user` rule never admits an unknown and N-1 senders land safe automatically without a compat flag. Honest limit, stated and never to be inflated: this is the honest member daemon's report — sound against agents on honest nodes (ancestry cannot be env-scrubbed away), NOT against a malicious node, and a detached process spawned outside an agent's session tree can launder to `user`. Defense-in-depth, not proof; a forged origin stamp sits at the same trust tier as a forged sender stamp and never elevates the subject chain. The `user-proven` TOTP-minted pid-linked token is FORWARD DESIGN and must not be built. Gate: doc — the CONTEXT.md origin-qualifier entry plus the amended wire-module identity-field doc carrying origin's forgery blast radius; impl — the two types, the rule field with its serde default, the request field, the chain's qualifier check, and the sending-daemon stamp; unit — the qualifier matches its own class and abstains on the other, `any` matches both, an absent request-side stamp evaluates as agent, an N-1 rule record with no qualifier decodes as `any`, and the wire field is additive in both directions exactly as the sender stamp is. [OK] REQ-ACL-POSITIONAL-ALLOW-HONORS-FLAGS required: [impl, unit, int] stages: -doc +impl +unit +int `access allow`'s POSITIONAL SPELLING REACHES THE SAME MUTATION SEAM ITS FLAG SPELLING DOES -- it is a shorthand for the rule, never a second, ungated way to write one. The v1 arm matched `node: Some(..)` and swallowed every other flag in a `..`, then called `AccessStore::allow` directly, and that cost three things at once, all three RUNTIME-MEASURED before the fix (releases#185; the filing was read-derived and named only the first). (1) `--surfaces MSG` WAS DROPPED, and an empty surface list means EVERY surface -- so the store took a rule strictly WIDER than the operator typed, exit 0, no diagnostic on either stream. The direction matters and was checked rather than assumed: the `is_empty => set_all(Mode::Closed)` at the tail of `allow_surfaces` reads `acl.modes`, the POSTURE table, and has nothing to do with the surfaces argument, so this falls as an over-grant and not as a silent close. (2) `--origin user` WAS DROPPED THE SAME WAY: a rule the operator restricted to one caller class matched both. (3) CALLING `allow` DIRECTLY WALKED AROUND `apply_mutation`, AND WITH IT BOTH THE `--admit-node` ACKNOWLEDGMENT AND THE GRANT-NODES POLICY -- the gravest of the three and the one no reading of the issue predicted. Measured as a three-arm differential under ONE authority (an owner agent, which requires a real perch record and not merely the env var): the flag spelling REFUSED the unacknowledged grant, the same flag spelling with `--admit-node` was accepted, and the positional spelling then wrote the WIDER rule for that same agent at exit 0. A gate a respelling steps around is not a gate, and the respelling was two words shorter than the gated one. THE FIX IS THE SEAM, NOT THE FLAGS: the arm builds its rule through the shared `build_access_rule` -- so the two spellings cannot drift into two parsers -- and applies it through `apply_mutation` with the invocation's real authority, which is what makes the acknowledgment and the policy bind here by construction rather than by a second copy of them. WHAT STAYS v1 IS THE POSTURE: this spelling RESTRICTS the endpoint (closes its blanket mode when it sets none yet), the seam is deliberately posture-blind, so the close is re-applied explicitly through one shared body (`restrict_if_unset`) that `allow_surfaces` also calls. `iff unset` is load-bearing -- an endpoint that already names a posture has been configured, and admitting one more node must never overwrite that. Whether the FLAG spelling should close a posture too is releases#196 and is deliberately NOT decided here; preserving this arm's close is a non-regression, not a new opinion. THE CONSEQUENCE IS ACCEPTED AND DECLARED (doyle, 2026-08-18): an owner agent running the bare positional spelling is now REFUSED without `--admit-node`, which is the gate working as designed and what the flag spelling already demanded; a same-node user is untouched, since the acknowledgment binds the owner-agent authority alone. Gate: impl -- the arm destructuring its flags instead of discarding them, the shared builder, the `apply_mutation` route carrying `admit_node` and the real authority, and `restrict_if_unset` as the single body of the posture-close; unit -- a decision table whose expectations are WRITTEN OUT AS LITERALS rather than derived from the code under test: the typed surface list lands on the persisted rule and an untyped one still means every surface, an origin qualifier survives, the unacknowledged owner-agent grant is REFUSED THROUGH THIS ARM while the same-node user's is not (the bypass, pinned as a refusal rather than as an audit), the refusal leaves the store BYTE-UNCHANGED, and the posture-close still happens for the positional arm while an endpoint that already names a posture keeps it; int -- THE JOIN, which is where this defect actually lived: the real binary, a real store on disk, the operator's typed list read back off the persisted RULE, with an untyped arm beside it (the empty list is a legitimate spelling and must survive the fix) and the unacknowledged owner-agent refusal paired with the same grant acknowledged, so the flag is the only difference between the two. [OK] REQ-ACL-PRECISE-ALLOW-WRITE-CONTRACT required: [impl, unit, int] stages: -doc +impl +unit +int THE PRECISE `access allow` IS A RULE VERB; THE POSITIONAL ONE IS A RULE+POSTURE VERB -- and the precise arm therefore says so at write time rather than leaving the operator to discover it. releases#196 asked whether the flag spelling should also close an endpoint's posture on its first rule (the v1 semantic `restrict_if_unset` carries, which the positional arm re-applies explicitly). RULED HORN B (doyle 2026-08-21, on a census read at b88fab2a): default-open-until-explicit-close IS the shipped T6 semantic, the mutation seam stays POSTURE-BLIND, and what was missing was never the close -- it was any signal that a rule written onto an unrestricted endpoint changes no verdict. TWO STRUCTURAL FACTS DECIDE THIS RATHER THAN A PREFERENCE. (1) A CLOSE CANNOT LIVE AT THE SEAM: `tuple_mutation` is shared by the endpoint precise arms AND the engine room's node-wide seat including its per-endpoint `--for` form, and below it `apply_mutation` is also the seam for knock-answer, redeem and the knock store -- so a close in `apply_mutation` would fire on every approved knock and every redeem, and a close in `tuple_mutation` would have the engine room closing another endpoint's posture as a side effect of writing one rule for it, while being meaningless for `MutationScope::Node` (`restrict_if_unset` is per-endpoint by construction). A posture close is a CLI-ARM opinion either way; it is not, and cannot be, a property of the seam. (2) THE CODEBASE ALREADY RULES THE INERTNESS IN BINDING GATE CODE: `may_grant_node_subject` derives the unset grant-nodes policy from `effective_posture` with the premise stated in its own comment -- open implies permitted, because an allow entry on an open surface punches no hole. Closing the posture inside the very command that policy gates would falsify that premise one statement later, so horn A would have owed a re-ruling of the grant-nodes policy and was never the one-line change it reads as. WHAT THE NOTICE IS. On an operator-driven precise allow, and BEFORE the write, the chain is ASKED -- `decide()` per covered surface, per admitted origin class, at every concrete origin there is to ask about -- and if it already allowed that subject everywhere, the acceptance is followed by a sentence saying the rule stands and changes no verdict today, naming the lever that would make it bite. NEVER A REFUSAL: pre-positioning a rule before a later close is legitimate and common, and a verb that refused it would be wrong more often than the silence it replaced. ASKED, NEVER RE-DERIVED: the condition is the chain's own verdict and not a scan of rule rows, the doctrine `node_still_sees_endpoint` and `fork_without_discover` already carry -- a row scan would miss a per-surface close, count a row naming a different subject, and become a second definition of a verdict the store owns. AN ABSENCE IS NEVER A VERDICT: a subnet subject with no roster members, and a sender-endpoint subject that resolves to no machine this node knows, have NO origin to put the question to, so they get a NAMED ABSENCE saying the question was not answered -- never the word inert, which would render a missing measurement as a finding. A subnet subject with members is inert only if EVERY member reads inert. THE FIRING POPULATION IS DECLARED AND PINNED, not left to placement: operator-driven precise ALLOW upserts on an endpoint scope, and nothing else -- the engine room's `--for` seat is excluded by the seat flag the caller already passes, removals and denies are excluded by op and decision, and knock-answer and redeem never reach this body at all. NO NOTICE ON DENY THIS LANE (doyle, ruled): deny is the fail-closed direction, an inert deny pre-positions defense at zero cost, and the narrowed-deny shape is un-censused. POSTURE-NEUTRALITY IS PART OF THE CONTRACT AND IS PINNED AS SUCH: the precise arm leaves `modes` exactly as it found them, which was true at b88fab2a and untested in either direction -- a ruling no test can falsify is a ruling that survives its own reversal. Gate: impl -- the population predicate, the chain-asked probe over resolved origins and its named-absence arm, the notice line naming the lever, and the call site before the write inside the precise arm; unit -- a decision table whose expectations are WRITTEN OUT AS LITERALS rather than derived from the predicate under test: an allow onto an unrestricted endpoint reads inert, the SAME allow shadowing a node-scope deny reads NOT inert (the hole-punch negative control -- the case the notice must stay silent on), a rule whose surfaces are only partly refused reads not inert, an empty subnet reads NAMED ABSENCE and never inert, a subnet whose members split reads not inert, and the population predicate admits the operator's precise allow while refusing the daemon seat, the removal and the deny; int -- the real binary against a real store: a precise allow leaves the endpoint's posture EMPTY (the literal partition, not a value read back from the code under test) and prints the notice, the positional spelling beside it still closes the posture and prints NO notice, and the NEGATIVE POPULATION PIN -- a rule written through the knock-approval path prints no notice at all. [OK] REQ-ACL-RC-VIEW-SPLIT required: [doc, impl, unit] stages: +doc +impl +unit -int Watching an endpoint's terminal and DRIVING it are separately grantable: attach gates on the request's AttachIntent — Viewer -> RC_VIEW, Control/Take -> RC_ATTACH. Before this, one access_check(endpoint, origin, Unsolicited) covered every attach intent, so admitting a node to view an endpoint necessarily admitted it to take the keyboard (and, with Take, to displace an incumbent controller). On a shared subnet that is the difference between showing a colleague's agent what happened and letting their agent drive yours. The split is keyed on the intent the REQUEST carries, evaluated at the serve side under the handshake-proven origin — never on anything the attaching side can restate after the gate. Kin: ADR-0042 (rc-attach truth) and REQ-ACL-SURFACE-VOCAB, which mints the two ids. Gate: doc — the CONTEXT.md control-surface entry naming RC_VIEW and RC_ATTACH as distinct v1 surfaces; impl — the attach_surface mapping and attach.rs gating through it; unit — the mapping over all three intents plus a decision table proving an RC_VIEW grant does not admit RC_ATTACH. [OK] REQ-ACL-RULE-MUTATION required: [] stages: -doc +impl +unit -int Access rule mutation and revocation are TUPLE-SHAPED, never id-shaped: a removal restates the subject, surfaces and origin qualifier rather than naming a generated rule id, which makes it idempotent and script-safe and removes rule-id bookkeeping from the model entirely (tuples ARE the identity). Every entry records its PROVENANCE — knock-approve, code-redeem, or manual — and the access drill-down view prints the exact removal command beside each rule together with that provenance, so revocation is copy-paste from the view that showed it. Entries born of knocks or codes have no special lifecycle after birth. Authority is threefold: the owner endpoint's agent (narrowing mutations always free, widening node-subject mutations in either polarity gated by the grant-nodes policy); same-node users always (node-sovereign, the emergency lever against a bad agent grant — there is no remote rule mutation of any kind, the remote-human path is to rc into the node); and the engine-room, which may edit node-tier entries, modes AND endpoint-scope entries across the node as the node's admin command center for access. That engine-room rule-EDITING authority supersedes the earlier 'engine-room owns modes only' clause; knock-ANSWER routing is unchanged — endpoint-target knocks still land with, and are answered by, the target endpoint. Every mutation path loads the store through the FAIL-CLOSED reader: a degraded store refuses the mutation loudly, leaves the store bytes byte-identical, and — where the mutation was a knock answer — leaves the knock PENDING rather than consuming it, because a refused write must never burn a single-use answer. Gate: doc — the CONTEXT.md rule-mutation entry and the CLI reference; impl — the tuple-matching removal primitive, the provenance field, the view's per-rule removal command, and the fail-closed load on every mutation path; unit — a tuple removal is idempotent and removes only the matching tuple, provenance survives a round-trip and renders in the view, the printed removal command actually removes the rule it was printed beside, a narrowing mutation needs no acknowledgment while a widening one does, and a mutation against a degraded store refuses loudly with the file bytes unchanged and any answering knock still pending. [OK] REQ-ACL-SUBJECT-CHAIN required: [doc, impl, unit] stages: +doc +impl +unit -int One FIRST-MATCH-WINS subject chain decides every access question, with an implicit-open bottom that preserves current fleet behavior. Order: per-endpoint sender-endpoint rule -> per-endpoint node rule -> per-endpoint subnet-wildcard rule -> node-scope node rule -> node-scope subnet-wildcard rule -> endpoint mode for the surface -> node mode for the surface -> join-time-captured subnet mode for the surface -> (nothing matched) OPEN. The stateful-firewall reply exemption and the same-node loopback Allow PRECEDE the chain and are not tiers — a degraded or locked-down node keeps its own conversations alive. Modes are the per-surface default posture for unlisted subjects (open/closed), settable at endpoint and node scope; the captured-subnet-mode table ships as a SCHEMA STUB in W1 (W2's join capture populates it — absent means the chain bottoms out open). Store schema v2 carries subject-kind entries at both scopes, migrates every v1 AccessEntry.nodes row into an equivalent (endpoint mode closed + one all-surface node Allow per listed node, so a listed node keeps access and an unlisted one loses it exactly as before), and DROPS the inert users field with the no-user-identity ruling (a shared subnet's trust unit is the node as human-proxy; 'user' is deliberately not a security identity). TIER 1 IS SCHEMA-REAL BUT INERT IN W1 (doyle ruling 2026-07-29, on todlando's pre-build finding): the design-of-record premise 'sender identity is daemon-stamped and nodes are trusted' is FALSE of its first clause today. The only wire record carrying a sender endpoint is WanMessage.from, which KNOWN-HAZARDS 7.5 (REQ-HAZARD-WAN-ORIGIN-AUTH) binds verbatim as reply-routing metadata 'never an authorization subject' — and it is caller-supplied (spt send --from; cli.rs resolve_from lets the explicit arg win over session detection), so keying a rule on it would admit a FORGED subject, the exact failure 7.5 exists to prevent. Precedent already ruled in-tree: REQ-MSG-5's classify_local_origin faces the same question and answers it with the session-proven perch id, noting '--from alone never confers an agent identity'. Independently, five of the six gate families (attach, xfer, rest, shell-link, digest) carry no sender endpoint on the wire in any form. So the kind persists, matches, and round-trips, but every W1 call site supplies None. Lighting it up needs an authenticated daemon-stamped sender field — a NEW additive field, never a repurposing of from — which is its own wave and its own REQ. Gate: doc — the CONTEXT.md endpoint-access-whitelist entry, whose false daemon-stamped premise this change corrects in the same diff, plus the control-surface-modes entry carrying the chain; impl — spt_store::access schema v2 (Subject/AccessRule/Modes/EndpointAcl/NodeAcl/CapturedSubnetModes), AccessStore::decide, the v1 migration, and the daemon gate threading origin subnet membership in; unit — the chain proven tier by tier (each tier decides only when every tier above abstains, bottom is open), the v1 verbs' polarity (allow restricts, revoke narrows without widening, open is the only widening), v1->v2 migration semantics, v2 round-trip of every construct, and TWO inertness guards — a store-level one proving the kind matches when fed and abstains when not, and a gate-level one proving no call site feeds it (that guard goes red on purpose if a later change threads a sender identity into the gate). [OK] REQ-ACL-SUBNET-MODE-CAPTURE required: [doc, impl, unit] stages: +doc +impl +unit -int A subnet's control-surface mode is recorded on the subnet and CAPTURED AT JOIN TIME into the joining node's own store — the W1 captured-subnet-mode table stops being a stub and lights up tier 8, the chain's bottom fallback (CONTEXT.md 'control-surface modes' resolution order). Capture, not lookup, is the point: a node's effective posture must be a local, immutable fact it consented to when it joined, so a subnet owner cannot retroactively tighten or loosen what an already-joined member enforces (that is what makes the advisory-gossip posture in REQ-ACL-MODE-ADVISORY-GOSSIP coherent rather than a race). Migration posture is explicit and must be tested: an EXISTING subnet has no captured mode, so tier 8 stays absent, the chain bottoms out open, and fleet behavior is preserved exactly — an upgrade must not silently close a running mesh. Gate: doc — the CONTEXT.md control-surface-modes entry (three levels and the resolution chain); impl — the subnet-mode record, the join-time capture into the endpoint access store, and tier 8 reading it; unit — a joined-with-closed node refuses where an unlisted subject falls through to tier 8, a joined-with-open node admits, and a pre-existing subnet with no capture bottoms open. [OK] REQ-ACL-SURFACE-ATTRIBUTABILITY required: [doc, impl, unit] stages: +doc +impl +unit -int Whether a control surface's inbound carries a PROVEN sender endpoint is single-sourced per-surface metadata, never a hardcoded MSG-vs-rest split. Attributability decides which SUBJECT TIER a grant for that surface can actually bind: attributable => a sender-endpoint subject can match (today MSG, via the W2b daemon stamp); non-attributable => only node-tier subjects are real, and a sender-endpoint entry on such a surface is dead on arrival — it can never match, so writing one is a silent no-op the CLI must refuse rather than persist. The single-source form is a per-surface table carrying the flag beside the id, with the v1 id list DERIVED from that table and exactly one reader answering the question; a second site that re-derives 'MSG or not' is the drift class this requirement exists to prevent (a property matched at one site and built at another). The payoff is automatic: when a surface's records grow their own sender stamp (XFER is the expected next), flipping one row makes it attributable and it exits every restriction keyed on this property with no re-ruling and no second edit. Gate: doc — the CONTEXT.md sender-attributability entry; impl — the per-surface table with the derived id list and the single is_attributable reader; unit — the derived list matches the table exactly, MSG reads attributable and a non-stamped surface does not, and flipping one row's flag moves that surface across every attributability-keyed decision without touching another site. [OK] REQ-ACL-SURFACE-DESCRIPTION required: [doc, impl, unit] stages: +doc +impl +unit -int Every control surface carries its operator-language DESCRIPTION as a third column of the same single-source surface table that already carries its id and its attributability, and each description states that surface's SUBJECT CONSEQUENCE rather than merely naming the traffic: an attributable surface says a grant binds the single proven sender ('MSG — direct messages (a grant binds the single sender)'), a non-attributable one says a grant admits the whole machine, and STOPS THERE: the remedy stays sited with the flag that owns it (the --admit-node help on knock approve and on endpoint access allow|deny|remove, plus the store's write-time refusal), because a second remedy spelling in a section that renders at a dozen sites is a drift pair — and would be outright FALSE at daemon access, a seat that acknowledgment does not bind ('XFER — file transfer (a grant admits the whole machine)'). Consequence, not category, is the load-bearing half: an operator choosing surfaces is choosing WHO gets admitted, and a description that omits that is decoration. The text is composed into every rendering AT RENDER TIME from the table, never restated at a display site, so the vocabulary a human reads and the vocabulary the gate enforces cannot drift apart and A NEW SURFACE APPEARS IN THE RENDERED HELP BY THE SOLE ACT OF EXISTING IN THE TABLE — that property is the point of the requirement and is proven directly rather than inferred from the shipped rows, which are uniform in it. Gate: doc — the CONTEXT.md surface-vocabulary entry carrying the consequence rule; impl — the description column beside the id and attributability flag, plus the one composer every renderer calls; unit — a fixture table carrying an INVENTED row renders that row with no edit to any display site, an attributable and a non-attributable row render their differing subject consequences, and the composed text is byte-derived from the table rather than matched against a second literal. [OK] REQ-ACL-SURFACE-MODE-VERB required: [doc, impl, unit] stages: +doc +impl +unit -int A NODE CAN CLOSE ONE SURFACE FROM ITS OWN ENGINE ROOM: `spt api access-node-surface-mode ` writes the NODE TIER's `per_surface` entry for exactly one control surface, and it is the ONLY off-switch a node owner has for a default-on surface. It exists because REQ-ACL-DISCOVER-DEFAULT-ON took the blanket posture out of a default-on surface's reach: after that change `access-node-mode closed` no longer closes DISCOVER, so the milestone's own promise -- a node owner who wants DISCOVER disabled can do so from their node's engine room -- is satisfied by this verb or by nothing (doyle 2026-08-17: shipping the carve-out without the off-switch is a greenlit-form deviation, and building it is the smaller act). THE NARROW TWIN, NOT A NEW AUTHORITY: it passes the SAME shared engine-room gate as `access-node-mode` (`api::engineroom::gate`, ADR-0052 decisions 5 and 6 -- one function, every engine-room verb through it, deliberately stated without a count) and needs NO empowerment, for the identical reason the blanket verb needs none: a node's own posture is its own business and the bring-up gate already proved membership. It lives in the `spt api` family rather than the rule-mutation family because that is where the gate, the auth proof and the ER capability vocabulary already are. NODE TIER ONLY: the endpoint tier's lever remains a deny row that names the surface, per-subject, until the concierge modernization mints its own form. THREE STATES, NOT TWO: `unset` REMOVES the entry, which is not a spelling of `open` -- an explicit open PINS the surface open against any later change of the vocabulary's default, so a two-state verb would make closing a default-on surface a one-way door whose only exit is a pin the writer never meant to author; the removal folds case for the reason every releases#66 reader does, since an entry stored as `discover` is the one that GOVERNS and a clear that missed it would report 'nothing was set'. THE PIN MUST BE VISIBLE (doyle's rider, 2026-08-17): an access view renders a pinned open distinguishably from a default open -- `DISCOVER open (pinned)` against `DISCOVER open (default)` -- because the same two words otherwise describe two different facts, and a blanket-closed summary that said only `closed` would read as closing DISCOVER too, which is exactly the reading the carve-out removed. The surface token is validated against `surface::TABLE` and canonicalized at the boundary: an unminted id stays legal in a RULE, where it governs only itself, and is REFUSED here, because this verb writes the key the chain READS and a typo would otherwise sit in the view as a posture that governs nothing. Gate: doc -- the CONTEXT.md mode-tier entry naming the verb as the node-tier off-switch and stating the three states; impl -- the store's node-tier per-surface setter, its case-folding clear and its read half, the api verb and its dispatch arm, the shared-gate call, the vocabulary refusal, and the pinned/default render in the access view; unit -- the verb writes only the named surface and leaves the blanket alone, `unset` restores the default rather than pinning open (with the default-on and ordinary cases asserted apart), a case-differing stored key is the one cleared, an unknown surface is refused with nothing written, a non-engine-room caller is refused by the shared gate, and the view renders pinned-open, default-open and closed as three distinguishable readings. [OK] REQ-ACL-SURFACE-VOCAB required: [doc, impl, unit] stages: +doc +impl +unit -int Access control is granular at the CONTROL SURFACE, not at the endpoint. A rule is (target endpoint x surface x subject) -> allow/deny, where a surface is a named remote-reachable operation class with an OPEN CONSTANT_CASE string vocabulary — new surfaces mint ids without a schema change, and an unrecognized surface string in a rule is legal (it governs nothing until that surface is built). The v1 set is the nine ratified ids: MSG, RC_VIEW, RC_ATTACH, DIGEST, WAKE, SUSPEND, XFER, SHELL_LINK, DISCOVER. WHAT THIS ENDS: the ADR-0009 whitelist was all-or-nothing per endpoint — admitting a node for messages also admitted it to drive the terminal, pull digests, and transfer files, because one access_check covered every wire-inbound family at once. On a SHARED SUBNET (member nodes belonging to different human operators, the gated adversary being agents) that coupling is the whole problem: there is no way to publish a view without handing over the keyboard. Each of the daemon's gate call-site families now tags itself with its surface, and the two families carrying a request-shaped distinction split: attach on AttachIntent (Viewer -> RC_VIEW vs Control/Take -> RC_ATTACH, see REQ-ACL-RC-VIEW-SPLIT), rest on the rest event (Wake -> WAKE vs Suspend -> SUSPEND). Gate: doc — the CONTEXT.md control-surface glossary entry; impl — the spt_store::access::surface vocabulary module, access_check's surface parameter, and the six call-site families tagging themselves; unit — the vocabulary shape (nine ids, CONSTANT_CASE, open to unminted strings) plus a per-surface decision table proving a grant on one surface does not admit the same node on another. [OK] REQ-ACL-VIEW-DRILLDOWN required: [doc, impl, unit] stages: +doc +impl +unit -int The granular rule list is viewable only per named ruled entity (CONTEXT.md 'access entity'): `spt endpoint access [--endpoint-rules | --node-rules | --subnet-rules ]` — mutually exclusive flags — renders exactly the rules whose subject names that entity, through the SAME ruleset-table renderer the engine-room briefing carries (REQ-ER-RULESET-TABLE), filtered on the TYPED rule subject rather than by re-parsing the rendered subject word (a rendered-string match is a second literal waiting to drift). A target positional scopes the slice to that endpoint's rules plus the node-scope rows that also decide for it; the drill's `--json` emits the rows verbatim. The one-flat-dump-of-everything view is deliberately gone — reading a posture is a comparison per entity, not a scroll. Gate: doc — the CONTEXT.md access-entity entry; impl — the typed filter seam and the flag surface; unit — the filter matches only its own entity kind (a subnet name equal to an endpoint id must not cross-match) and the flags parse mutually exclusive. [OK] REQ-ACL-VIEW-ROSTER required: [doc, impl, unit] stages: +doc +impl +unit -int Access views are roster-first (CONTEXT.md 'access entity', FF grill 2026-07-30): `spt endpoint access []` lists each ruled target's access entities — the subnets, nodes, and endpoints its own rules name — grouped by type in that order, each with its rule count; a subnet or the home node also carries its mode summary; a captured-mode subnet is listed even with zero explicit rules (a mode source is part of the posture picture); external entities with no rules for the target are omitted entirely; a ruled endpoint resolves to `@ ()` where this node's registry can see it and renders bare where it cannot (ruled-but-unresolvable must not hide the rule count). The view is node-sovereign — no remote rule-read exists; viewing another node's rules means running the CLI there. Supersedes the `access list` prose view; the `--json` surface re-homes the old shape's explicit `locked` flag onto the roster envelope. Known limit, documented: an endpoint literally named after a write verb (allow/revoke/open) parses as the subcommand and loses the positional path. Gate: doc — the CONTEXT.md access-entity entry; impl — the roster builder, renderer, endpoint directory, and the superseding CLI surface; unit — the roster grammar (grouping order, counts, mode annotations, bare-vs-resolved endpoints, explicit empty line) and the parse surface. [OK] REQ-ACTIVITY-INFO-PULL required: [doc, impl, unit] stages: +doc +impl +unit -int The endpoint's current activity state (busy|idle) is readable via `spt api endpoint-info` — a point-in-time read of the perch idle sentinel, for consumers that need a check rather than a stream (ADR-0048 decision 1, pull avenue; operator-ruled 2026-07-24). Additive key, N-1-safe per the additive-evolution posture. [OK] REQ-ACTIVITY-LINK-PUSH required: [doc, impl, unit, int] stages: +doc +impl +unit +int An owned Shell observes its owner's busy/idle transitions as an activity frame on the EXISTING shell-link event stream (ADR-0048 decisions 1-3; perri contract-lock 2026-07-24). Link-scoped (owner implied by the link token — no new verb, no addressing surface). Drive-class semantics: ephemeral, latest-wins, current-state-carrying; redundant same-state resend is a harmless no-op (consumer derives edges); NEVER spooled or replayed (stale transitions are actively wrong). Both directions (busy->idle, idle->busy). Current state emitted on EVERY link establishment and re-link — restart resync is free. Latency promise = BOUNDED OBSERVATION: a frame per transition, sub-second class, never hard-real-time; each frame carries the TRANSITION TIMESTAMP (when the sentinel flipped, not when the frame was emitted) so edge-anchored consumers self-correct emission latency. Frame name/attrs publish in the shell frame vocabulary doc (REQ-SHELL-FRAME-VOCAB lineage) — doc stage rides there. Gate: int — a live shell link sees busy->idle and idle->busy frames with truthful transition timestamps, a re-link re-emits current state, and a transition during link-down is represented by the re-link emit (current state), never a replayed backlog. [OK] REQ-ACTIVITY-LIST-JSON required: [doc, impl, unit] stages: +doc +impl +unit -int `spt endpoint list --json` carries a per-endpoint `activity` (busy|idle) key, for consumers surveying the idle/busy state of many endpoints at once (ADR-0048 decision 1, roster pull avenue; operator addition 2026-07-24). Additive key, N-1-safe; kin the flynn 2026-07-06 last-active/description/adapter list-enrichment seed (same additive posture, may ride together). [OK] REQ-ADAPTER-ADD-SURFACE-ERRORS required: [impl, unit] stages: -doc +impl +unit -int Bug #1: adapter add runs the install-as-first-update via conduct (cli.rs:6963) which on a non-zero exit prints only the exit code and DISCARDS the subprocess stdout/stderr, so the real error is invisible (the failure itself does propagate). Fix: include out.stderr/stdout in the ADAPTER_INSTALL_FAIL message (mirror run_update_post_step). Operator ruling: ALSO run the [update.post] composite step at install-time (today it runs only on explicit adapter update), so an install both surfaces detail and completes the delegated post-step. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #1. [OK] REQ-ADAPTER-FLOOR-ENFORCE required: [doc, impl, unit] stages: +doc +impl +unit -int F-5 (REMOTE-TRUTH triage §F-5 + doyle rulings 2026-07-05): BOTH adapter acquisition verbs (spt adapter add + spt adapter update) REFUSE when the installed spt-core is BELOW the adapter's declared [adapter].min_spt_core_version floor — with an F-1 operator refusal naming the installed core, the floor, and the next action (update spt-core first). ROOT: the floor was PARSED + required (manifest.rs) but never compared to the running core — dead enforcement; and the [update].version_check knob that gated it was DOC'D-BUT-DEAD (never read by any production path — a contract lie). RULINGS: RETIRE version_check (drop the manifest field + schema + docs + the cfg(test) literals; a pre-existing manifest still setting it deserializes fine — serde ignores the unknown key, no deny_unknown_fields, so retiring is back-compatible); SEMVER-compare NOT string-compare (the 0.9.0 < 0.25.0 lexical trap); enforce on BOTH verbs; nothing installs / registry untouched on refuse (binds both verbs, no residuals). FIX: (1) a pure spt-runtime version_meets_floor(core, floor) -> bool (numeric per-component: split '.', u64, missing→0, non-numeric→0, first-diff decides, equal-when-zero-padded ⇒ satisfied) — mirrors the CLI version_is_newer parse (same numeric model, different question: freshness=strictly-newer vs floor=at-least). (2) ADD: the gate lives INSIDE registry::register (the choke point) via a register_with_core(core_version) seam register() delegates to with env!(CARGO_PKG_VERSION) — the floor check runs right after the manifest parse, BEFORE any registry write, returning the typed RegistryError::CoreFloor{adapter,core,floor} (Display = the ONE F-1 refusal both verbs surface); nothing recorded on refuse. (3) UPDATE: a PRE-SWAP peek (staged_floor_ok) extracts the staged .spt to a THROWAWAY temp, parses its manifest floor, and refuses BEFORE apply_release_crc_swap mutates the live pointer-mode home — so a refusal (or an unverifiable floor: FAIL-CLOSED) leaves the live install BYTE-UNTOUCHED; register@8932 stays as the defense-in-depth backstop for every other entry path. THE COMPARISON BASIS IS THE CALLER'S, NOT THIS FUNCTION'S (releases#159): the bare verb judges against the running CLI, and a COMPOSITE update judges against the core that run will activate — see REQ-ADAPTER-FLOOR-VS-STAGED-CORE. The FAIL-CLOSED discipline in this clause is about an unverifiable STAGED MANIFEST and is unrelated to that input. doyle bind: the register-only gate would let the crc-swap replace the live files with a floor-violating version while the record refuses (record and reality disagree — the exact contract-lie shape this milestone kills), so the pre-swap peek is the only correct answer. Red-first: perri negative repro on ADD (fresh home + synthetic low core + high-floor manifest → CoreFloor refuse, registry untouched) + the UPDATE pre-swap refuse (live home byte-untouched) + a floor-met positive control (0.25.0-on-0.25.0 installs); + version_meets_floor table incl. the 0.9<0.25 trap. [OK] REQ-ADAPTER-FLOOR-VS-STAGED-CORE required: [doc, impl, unit] stages: +doc +impl +unit -int IN A COMPOSITE UPDATE, AN ADAPTER'S CORE-VERSION FLOOR IS JUDGED AGAINST THE CORE THAT RUN WILL ACTIVATE, NOT THE ONE CURRENTLY RUNNING (releases#159, doyle ruling 2026-08-21). THIS DELIVERS A CONTRACT ALREADY RULED AND NEVER SATISFIED, which is what the lane is FOR: CONTEXT.md's adapter-manifest-header entry has said since the glossary was written that `when spt-core self-updates, re-verify adapters still satisfy (coordinate core + adapter updates when needed)` — the code never did. Same shape as F-5 itself (REQ-ADAPTER-FLOOR-ENFORCE), where the floor was PARSED and REQUIRED and never compared: a dead rule, not a missing one. THE DEFECT: `spt update` plans FetchApply -> Adapters, and `spt update --restart` plans Fetch -> Adapters -> Finish; in BOTH the adapters leg runs while the NEW core is staged and the invoking CLI is still the OLD image, so a floor comparison against `env!(CARGO_PKG_VERSION)` asks a question about a core that is on its way out. An adapter whose floor the INCOMING core satisfies is therefore refused in-cycle, and the run then activates that very core and leaves the OLD adapter sitting on it — the refusal protected nothing, it deferred the pairing it was meant to enforce, ungated, into a state nobody checks again. FIX: `cmd_adapter_update` takes the comparison version as a PARAMETER instead of baking `env!`; the composite leg passes the STAGED release's `product_version`, read from the VERIFIED signed metadata via `ReleaseCache::staged_update` (`UpdateSetMetadata` / `ReleaseMetadata`). SINGLE-PASS, NOT A RE-SWEEP (doyle): a post-finish second sweep can itself fail half-rolled, so the one pass judges against the truth it already holds — and the CONTEXT.md clause above is amended to state that cadence, because a glossary sentence whose cadence a future implementer could build against would authorize the refused re-sweep with the glossary's own authority behind it. BOTH COMPARISON SITES INSIDE THAT FUNCTION TAKE THE PARAMETER (doyle confirmation 2026-08-22, dated to the confirmation and not to the lane: the correction postdates the ruled shape it corrects) — (a) the pre-swap peek `staged_floor_ok`, and (b) the post-swap re-register, which moves from `registry::register` (the `env!`-baking wrapper) to the already-public `registry::register_with_core` seam beneath it. (b) IS LOAD-BEARING, NOT TIDINESS: fixing only (a) admits the adapter at the peek, crc-swaps the new bits into the live pointer-mode home, and THEN refuses at the record against the old running core — live files new, registry record old, reported as FAILED. That is the record-and-reality disagreement REQ-ADAPTER-FLOOR-ENFORCE's own pre-swap-peek bind exists to prevent, reached from the other side, and it is strictly worse than the bug this requirement fixes (today's refusal is clean and byte-untouched). One value, two consumers, SO THE PEEK AND THE RECORD CANNOT DISAGREE ABOUT WHICH CORE THEY JUDGED AGAINST — that property is what makes this a fix rather than a second gate to keep in sync, and it is why the parameter is THREADED rather than each site reading the staged metadata for itself. THE BARE VERB IS UNCHANGED: `spt adapter update` (cli.rs AdapterCmd::Update) and its `spt update adapters` alias keep passing the running CLI's version — with no update in flight the installed core IS the right question — and both call sites are pinned as unchanged. `spt adapter add` is untouched: no staged core exists on that path. FAIL OPEN, ON THE NEW INPUT ONLY (doyle): an unreadable staged version degrades LOUDLY to the running-CLI comparison — an absent answer is not a zero, and an empty string is never compared against a floor — while `staged_floor_ok`'s deliberate FAIL-CLOSED arm for an unverifiable STAGED MANIFEST floor (REQ-ADAPTER-FLOOR-ENFORCE) is NOT touched. ONE ARM, THREE DIAGNOSTICS (doyle Q1 ruling, extended 2026-08-22 by measurement): an absent stage, an empty `product_version` (`#[serde(default)]` on both metadata types — pre-v0.3.2 releases really do parse to an empty string), and metadata that parses as NEITHER type all take the SAME degrade to the running core but must NOT share a message, because the operator's next step differs: nothing to do, an old release, and inspect your stage. The third states the FACT and points AT the stage without diagnosing it, borrowing 'malformed' from the apply leg's own vocabulary so two arms of one run never name the same corrupt artifact differently; the second offers predating-the-field as the LIKELY cause rather than the certain one, since any producer omitting the key yields the identical empty string. MERGING THE THIRD INTO THE SECOND IS SPECIFICALLY FORBIDDEN: telling an operator their release predates a field when the artifact is DAMAGED is a benign explanation offered for a corruption condition. THE THIRD ARM IS DEFENCE IN DEPTH AGAINST A STATE THE RUN SHOULD ALREADY HAVE REFUSED, and this is recorded here because the arm will otherwise look like dead code to the next reader and be deleted: in a COMPOSITE it is almost unreachable BY CONSTRUCTION — the core legs reject a malformed stage as a FAULT (RejectReason::Malformed, UPDATE_APPLY_REJECTED, exit 1 and not 3, since an integrity failure is not a policy decline) and composite_abort_on_failure is true for BOTH FetchApply and Fetch, so the run aborts before an adapters leg exists. It is reached only when a core leg exited 0 over an already-corrupt stage (a fetch with nothing new to pull) or when the stage changed after that leg ran, which is why its sentence names both possibilities and asserts neither. MEASURED REACHABILITY, one door only (relcache.rs:330-351): corruption at the SignedRelease envelope dies at `.ok()?` and yields None, indistinguishable from no stage and correctly reported as such — only inner metadata that parses as neither type, WITH an artifact on disk, falls to the `else if` and reaches the seam as Some. READING `product_version` HERE DOES NOT VIOLATE ITS OWN WARNING, stated in this title so the next reader meets the warning before they meet the use: that field's doc forbids using it to decide WHICH RELEASE IS NEWER — the monotonic `version` counter is the ordering authority and this requirement does not touch ordering — it does not forbid reading it as the semver a declared floor is compared against, provided it comes from the verified signed metadata, which is the only place this reads it from. Gate: doc — the CONTEXT.md adapter-manifest-header clause restated as the ruled cadence (the sentence this requirement delivers), the CONTEXT.md update-composite entry's floor-gate phrase, and MANIFEST.md's enforcement note, each naming WHICH core the floor is judged against; impl — the comparison-version parameter on `cmd_adapter_update` threaded to both the peek and `register_with_core`, the composite leg's staged-version read off the verified metadata, and the two-diagnostic loud degrade; unit — an adapter flooring on the FETCHED core is ADMITTED by the composite AND its record registers at the new version, the bare verb and its alias still compare against the running CLI, and the empty-`product_version` and absent-stage degrades each render their own diagnostic and fall back rather than refusing. [OK] REQ-ADAPTER-GH-TRANSPORT required: [doc, impl, unit] stages: +doc +impl +unit -int The `gh_release` avenue (and `spt adapter add --release`) gain a fetch `transport`: `https` (current reqwest direct, public), `gh` (shell the pre-authorized `gh` CLI — the private-repo path; `gh` honors OAuth and `GH_TOKEN`, so spt custodies no token), or `auto` (default: prefer `gh` when installed+authed, else HTTPS). `--gh`/`--https` force it on `add`. Additive over the existing fetch path; verify->extract->register downstream is unchanged. (v0.13.2) [OK] REQ-ADAPTER-LIVE-UPDATE required: [doc, impl, unit, int] stages: +doc +impl +unit +int An adapter update is live and daemon-coordinated (the adapter analog of brain self-update, ADR-0004): for an endpoint with a running RESIDENT adapter binary (today the `[message-idle-translation-binary]`), the CLI keeps fetch+verify and hands the APPLY to the daemon over IPC, which per affected endpoint (1) STOPS the resident binary -> releases the OS file lock (fixes the Windows 'Access denied (os error 5)' overwrite failure), (2) swaps on disk ONLY files whose CRC differs from the staged archive (unchanged files + their still-running binaries untouched), (3) RE-CLONES the new on-disk manifest into the running `BrainLifecycle` (the in-memory manifest is cached at bringup and otherwise goes stale -> binaries+manifest back on the same page), (4) RESTARTS the resident binary from the new files. An endpoint NOT running -> CLI swaps directly (no lock, no cache). Only the resident class is cycled; ephemeral adapter binaries (Psyche loop, `[digest]` extractor, `[session.*]` runners, hooks) self-heal on next spawn and are excluded. The daemon keeps a per-endpoint registry of resident adapter children. (ADR-0025, v0.13.2) [OK] REQ-ADAPTER-MULTIPLATFORM-SPT required: [doc, impl, unit, int] stages: +doc +impl +unit +int A `.spt` adapter archive may pack multiple platforms in one signed asset: shared `manifest.toml` + `strings/` at the root, role binaries under per-Rust-target-triple subdirectories (ADR-0016 triple vocabulary, e.g. `x86_64-pc-windows-msvc/`); install/update extracts the shared root plus ONLY `current_platform()`'s triple subdir, flattened into `install_dir` so flat `/` resolution (REQ-INSTALL-11) is unchanged. Name stays `adapter.spt` (plain-tar or gzip, `--asset` optional default); one whole-archive Ed25519 signature over the fat archive (REQ-UPD-9 single-artifact verify). A legacy flat archive (no triple subdirs) extracts as today (free back-compat); a multi-platform archive sets `min_spt_core_version >= 0.13.2` (forward-compat gate, readable before extract); a multi-platform archive missing the recipient's triple -> typed `NoArtifactForPlatform`, never a silent no-op. Large adapters may still split per-platform (single-triple archives via `--asset`, or ADR-0016 update-set machinery). (ADR-0024, v0.13.2) [OK] REQ-ADAPTER-PROOF-DIR-OVERRIDE required: [doc, impl, unit, int] stages: +doc +impl +unit +int The author-time proof commands (`spt adapter digest-proof`, `spt adapter translate-proof`) gain a `--dir ` / `--manifest ` override so an author proofs a DEV binary against an on-disk manifest+install dir WITHOUT staging a full extracted GhReleaseManaged install (mirrors digest-proof's `--sample` pointing straight at a file). Fixes perri F-011: a bare-file-added gh_release adapter currently can't be resolved by the *-proof commands ('manifest is not present yet at '); un-stales the bare-file digest-proof int. (perri F-011, v0.13.x DX) [OK] REQ-ADAPTER-TEMPLATE-KEY-VALIDATION required: [impl, unit, int] stages: -doc +impl +unit +int P-1 core half (WORKER-TRUTH triage): `adapter add`/`adapter update` validate every declared role template — command, cwd, and [env] inject values — against the substitution-key catalog (spt-runtime BASE_KEYS + role-specific overrides) and REFUSE registration naming the offending key + role (fail-fast family of the [strings] pointer validation). Field driver: flynn's psyche died on the RETIRED {psyche_dir} key (adapter psyche_resume carried the old psyche_init cwd shape) — a permanent template config fault must die loudly at registration, not at the Nth per-event psyche turn via the 3-strike budget. The catalog stays the single source (runtime.rs FILL_KEYS — 'a catalog key must have a real fill'); validation reads it, never a second list. [OK] REQ-ADAPTER-TRANSLATE-PROOF required: [doc, impl, unit, int] stages: +doc +impl +unit +int `spt adapter translate-proof --event [--session ]` — the author-time EMIT-half proof tool for `[message-idle-translation-binary]` (ADR-0022), symmetric to `spt adapter digest-proof` (REQ-TERM-5). It spawns and feeds the adapter's declared translation binary EXACTLY as the daemon does at idle-delivery — running the REAL `spt_daemon::translation` driver VERBATIM (no protocol reimplementation): `TranslationChild::spawn` the binary, send the `{type:"init",endpoint_id,node}` line then the `{type:"event",envelope}` line, and read back the emitted `{key}`/`{text}`/`{delay_ms}`/`{commit}` keystroke-command stream — then prints it author-readable (each Key with its `key_to_bytes` rendering, Text quoted, Delay in ms, Commit marker) with counts. It fills the SAME `{id}`→option and `{session_id}`→(--session, else a placeholder) keys into the `--event` envelope the daemon fills at runtime, so an envelope that proofs here feeds faithfully live. EMIT-half ONLY: it proves the binary's spawn+feed+emit contract; it does NOT exercise the daemon's atomic PTY apply / controller-buffering (that stays covered by the W2 inject_control_wedge int gate) — `--help` says so. Exit codes mirror digest-proof: 0 ok, 1 on spawn-fail / zero commands / no-commit-or-output / unparseable, 2 when the adapter declares no `[message-idle-translation-binary]` section. The `TranslationChild` Drop does the bounded no-zombie reap. (v0.13.x) [OK] REQ-ADAPTER-UNRESOLVED-HINT-FORM required: [impl, unit] stages: -doc +impl +unit -int F-034 leg a (perri/hertz field finding 2026-07-09): the ADAPTER_UNRESOLVED refusal hint must print a WORKING command form. It currently says 'pass --adapter ', but --adapter is a `spt api` GROUP flag, NOT a `listen` flag — following the hint literally (`spt api listen --adapter `) produces clap `error: unexpected argument '--adapter'` (exit 2). Fix: the hint prints the group-level form, e.g. `spt api --adapter …` (a hint the operator can copy-paste and have work). Gate: the ADAPTER_UNRESOLVED message text carries a clap-VALID invocation (group-level --adapter placement) — a unit asserting the hint string parses under the api clap grammar, or at minimum places --adapter before the subcommand. Pure UX/hint-correctness fix, no behavior change. [OK] REQ-ADAPTER-UPDATE-INPLACE required: [impl, unit] stages: -doc +impl +unit -int Bug #18: spt adapter update fails at re-register with os error 2 because it derives the install dir from the update repo NAME (_github/) instead of updating in place at the adapter record source_dir; when the adapter repo is intentionally renamed across releases (spt-claude-code to claude-spt, supported), the derived dir is fresh/empty and re-register reads a missing manifest. Fix: adapter update installs and re-registers in place at the registered source_dir and tolerates a changed update repo/URL across a rename. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #18. [OK] REQ-ADAPTER-UPDATE-MESSAGE required: [doc, impl, unit] stages: +doc +impl +unit -int An adapter manifest may declare `[update].message` — a plain (multi-line) human notice surfaced to stdout, markdown-rendered (the v0.13.0 helpfmt prose path), ONLY when `spt adapter update` actually APPLIES an update (version changed), not on a no-op. Read from the newly-installed manifest; avenue-agnostic (gh_release/delegated/file_pull). No `{key}` substitution. Use: an adapter telling the operator a post-update action, e.g. spt-claude-code's "run `/reload-plugins` in any ongoing sessions". (v0.13.2) [OK] REQ-ADAPTER-UPDATE-POST required: [doc, impl, unit, int] stages: +doc +impl +unit +int Composite adapter update — an avenue-agnostic `[update.post]` sub-table `{ command, self_verifies }` run AFTER the primary avenue (gh_release/file_pull/delegated) resolves, in the same `spt adapter update` (ADR-0029). Runs UNCONDITIONALLY (even on an adapter version no-op — the post-step's own idempotent check decides). PUBLISHED stdin JSON seam: one line `{adapter_applied, adapter_name, profile_name, version, previous_version, adapter_dir}` (additive keys; post-step ignores unknown). stdout decides the notice: custom text SUPERSEDES [update].message; a reserved sentinel fires the static [update].message; empty = no notice. exit code orthogonal (0 ok / nonzero failed). Precedence: dynamic-stdout > sentinel/manifest-message > nothing. NO [update.post] declared ⇒ today's adapter_applied→[update].message unchanged; post-step FAILS ⇒ loud warning + fall back to adapter_applied→message. FAILURE-ISOLATED: a committed gh_release pull is never rolled back if the post-step fails (independent channels). (v0.16.0) [OK] REQ-ADAPTER-VERSION-CMD required: [doc, impl, unit] stages: +doc +impl +unit -int `spt adapter version ` prints a registered adapter's declared version — the EXISTING mandatory `[adapter].version` manifest field (manifest.rs already requires it; NOT a `[strings].version`, NOT `get-string`, no second version source). A new `AdapterCmd::Version{option}` resolves the option's merged view via `registry::resolve_option` like the sibling adapter subcommands and prints `manifest.adapter.version`; an unresolvable option errors (exit 1) the same way. (v0.13.2 W6) [OK] REQ-API-1 required: [impl, unit, int] stages: -doc +impl +unit +int api prefix and adapter_name on every machinery invocation [OK] REQ-API-2 required: [impl, unit, int] stages: -doc +impl +unit +int The api subcommand surface (bind/listen/poll/state/worker/boundary/...) [OK] REQ-API-3 required: [impl, unit, int] stages: -doc +impl +unit +int commune/signoff are file-drops, not commands [OK] REQ-API-4 required: [doc, impl, unit] stages: +doc +impl +unit -int api resolves the adapter manifest (+ profile + install dir) from `--adapter name:profile` via the registry when `--manifest` is omitted; `--manifest` becomes an optional OVERRIDE (unregistered / local-dev manifests). Removes the require-both-flags redundancy — a registered adapter's live bringup / digest / capability needs only `--adapter` — and yields the precise install dir (the record's source_dir) rather than the --manifest parent, closing the copy-mode psyche-binary edge (v0.8.0) [OK] REQ-API-ENDPOINT-INFO required: [impl, unit] stages: -doc +impl +unit -int #7: spt api endpoint-info [] (JSON) lets an endpoint learn its ATTACHED (controlling) node — claude-spt surfaces local + attached node names on UserPromptSubmit so the agent knows whether getting a file to the user needs extra steps (user RC'd in from another machine). spt api * is the harness-contract agent-facing surface (JSON-first, rides perch identity/auth so the bare no- form self-resolves like whoami). Payload (committed DTO, additive-forever): { id, endpoint_type, adapter, local_node:{label,key}, attached_node:{label,key}|null, controlled:bool, project:, cwd, subnets:[...] } — attached_node from controller stamps (driven_by remote / self-node when controlled with no remote driver), null when uncontrolled. HARD dependency on #2 + #3 (stamps must be honest first). Adapter-side consumable -> perri release-ping on publish. Naming: chose 'spt api endpoint-info' over alt 'spt endpoint get-info' — api is the agent surface (doc rationale). See docs/NEXT-MILESTONE-PICKER-TRIAGE.md #7. [OK] REQ-ARCH-1 required: [impl] stages: -doc +impl -unit -int Many small acyclically-layered crates [OK] REQ-ARCH-2 required: [impl] stages: -doc +impl -unit -int Public SDK surface is spt-proto, spt-runtime, spt-msg [OK] REQ-ARCH-3 required: [impl, unit] stages: -doc +impl +unit -int Wire-protocol version independent of crate semver, N-1 compat window [OK] REQ-ARCH-4 required: [impl, unit] stages: -doc +impl +unit -int Copy-verbatim the commodity layer from the sister project [OK] REQ-ATTACH-AWAY-ALERTS required: [doc, impl, unit] stages: +doc +impl +unit -int AN ENDPOINT LEFT UNATTACHED FOR FIVE MINUTES IS TOLD SO, ONCE, ON THE HOOK CHANNEL, AND TOLD AGAIN WHEN SOMEONE COMES BACK -- BUT ONLY IF IT WAS TOLD THE FIRST TIME (releases#45, rides REQ-ATTACH-LINK-PUSH). THE AWAY TEXT IS THE TICKET'S, VERBATIM, AND IS NOT OURS TO REWORD: it tells the agent to proceed but not to assume the user can see its output, to WITHHOLD user-aimed information until a controller or viewer attaches, that it will be notified when that happens, and -- second paragraph -- to share such information with viewed-and-relevant same-node endpoints it is actively collaborating with while avoiding unwarranted comms. It lives as ONE CONSTANT so the wording has a single home and its verbatim-ness is auditable at a glance rather than reconstructed from a formatter. IT IS SENT active_only, WHICH IS THE POINT AND NOT A DETAIL: active_only is the spool-only hook channel with NO live TCP and NO wake, so a message about nobody watching cannot itself be the thing that wakes an agent -- it arrives at the next turn boundary, which is the only moment the agent can act on it anyway. FIRE-ONCE LIVES IN THE CONDITION, NEVER IN A NOW-SIGNAL SEEN-SET (doyle, 2026-08-30): the away/return pair is delta-disciplined by construction -- away fires on the unattached edge at five minutes and latches, return fires on the re-attach edge and clears the latch -- so borrowing a seen-set would put the same fact under two disciplines and make which one is authoritative a question. THE RETURN NOTICE FIRES ONLY IF AWAY FIRED: an agent that was never told nobody was watching must not be told someone came back, because the second message is meaningless without the first. Its wording is NOT fixed by the ticket, which asks only for a short reciprocal message that may assume the agent remembers the away context -- so it is authored here, short, and deliberately does not restate what the away message said. RE-ATTACHMENT IS ANY ATTACHMENT: a controller or a viewer, from any node, ends the away condition, because the away message's own promise is about a controller OR viewer attaching and a notice that honoured a narrower rule than the promise would be a broken promise. [OK] REQ-ATTACH-CLIENT-STALE required: [doc, impl, unit, int] stages: +doc +impl +unit +int Known attach clients below a release-maintained recommended minimum receive ATTACH_CLIENT_STALE only for an applicable platform and attach role with a concrete demonstrated degraded-function reason (releases#230, REMOTE-FRICTION #304). Both controllers and read-only viewers receive delta-only guidance, without forced detach. Unknown client versions and ordinary version skew remain silent. The running client identity is distinct from broker identity. Advise reopening when a sufficient binary is installed on the client node; otherwise advise client update and reopen, never a daemon restart for client-only skew. [OK] REQ-ATTACH-IDEMPOTENT-REPLAY required: [impl, unit, int] stages: +doc +impl +unit +int The equal-generation lease rung is idempotent for the same connection. (ADR-0047 decision 3, AMENDING ADR-0044's ladder inside the equal-gen rung; hertz v0.39.4 field bug 4, PINNED via OBS breadcrumbs on authorized same-seam `daemon refresh` 2026-07-22 — gen+1 premise FALSIFIED.) TODAY: a Control/Take subscribe with same identity + same nonzero gen classifies 'same lease, silent re-take' (broker.rs equal-gen branch — correct, no revoke) but re-take = become_controller, which unconditionally takes+drops the prior seat (writer exits channel-closed) with NO same-conn check — designed for the dead-seat dispatcher-restart successor, it also fires against the SAME LIVE conn re-served 15ms apart by post-cycle dispatcher replay: the lease kills its own writer, the rc viewer freezes until detach+re-attach (the field 'update freezes PTYs'). FIX: keyed (endpoint/session, by, conn, gen) — same-conn equal-gen = IDEMPOTENT REPLAY: seat + writer PRESERVED, no controller-replaced, no second initial batch; breadcrumb answers decision=idempotent (additive vocab). Equal-gen DIFFERENT-conn keeps today's silent swap (the ADR-0038 fix-6 successor — must not regress); strictly-newer keeps loud supersession, strictly-older keeps busy. BANKED open question (build-time look, not a blocker): why the dispatcher double-served one held attach 15ms apart — may have siblings. Gate: impl — the same-conn discriminator in resolve_subscribe's equal-gen branch + the idempotent decision label; unit — the four ladder cells (same-conn idempotent / different-conn swap / newer supersedes / older busy) against a mock seat; int — the bug-4 rig: replay one held Attach after a brain refresh, assert ONE writer + zero controller-replaced + the original rc interactive (RED-first), plus the different-conn successor leg stays green (redispatch/brain_swap suites). [OK] REQ-ATTACH-LINK-PUSH required: [doc, impl, unit, int] stages: +doc +impl +unit +int AN OWNED SHELL OBSERVES ITS OWNER'S ATTACHMENT STATE AS AN ATTACHMENT FRAME ON THE EXISTING SHELL-LINK EVENT STREAM (releases#44). The SIBLING of REQ-ACTIVITY-LINK-PUSH in every structural respect, and deliberately so: link-scoped (the owner is implied by the link token, no new verb and no addressing surface), drive-class semantics -- ephemeral, latest-wins, CURRENT-STATE-CARRYING, so a redundant same-state resend is a harmless no-op the consumer derives edges from -- NEVER spooled and NEVER replayed, because a stale attachment is actively wrong rather than merely old. Current state is emitted on EVERY link establishment and re-link, which is what makes restart resync free. Bounded observation, sub-second class, never hard-real-time. IT GETS ITS OWN DriveHub SLOT CLASS: latest-wins is only correct WITHIN a class that supersedes itself, so an attachment push must never be able to eat an undelivered drive command or an activity frame, nor they it. WHAT THE FRAME CARRIES: whether a controller is attached and from which NODE, how many read-only viewers are attached and from which NODES, and which node CHANGED on this edge. THE VIEWER NODE LIST IS RETAINED IN-MEMORY ON THE BROKER'S ViewerSink FOR FRAME COMPOSITION ONLY, and that bound is part of the requirement rather than an implementation note: THE DURABLE CONTROL TRIPLE (driven_by / controlled / viewer_count) IS UNCHANGED, and durable viewer identity, if it is ever wanted, is ITS OWN REQUEST (kin: the register's IR-72 custody-surface gap -- noted in a comment, never built toward). The measurement that justifies retaining it at all: the viewer's origin node ALREADY REACHES the broker on the attach path (attach.rs attach_as_viewer_snap -> brain.rs subscribe_with `by`) and is DROPPED on the floor by a ViewerSink that keeps only tx/send/evicted/writer -- reachable-and-discarded on the correct write path, which is materially different from a source that was never built in the arm at all (releases#11b) and is why this is a retention rather than a board request. Gate: int -- a live shell link sees an attachment frame on controller attach and detach and on viewer attach and detach, naming the changed node; a re-link re-emits current state; and an attachment change during link-down is represented by the re-link's current-state emit, never by a replayed backlog. [OK] REQ-ATTACH-RESIZE-REPAINT required: [unit, int] stages: +doc -impl +unit +int RESCOPED 2026-07-22 by ADR-0047 Amendment 2 (doyle-ratified): this REQ no longer asserts a defect. Its original claim — that the attach-stream resize never arms the presentation barrier, and that this is v0.39.4 field bug 3's root — is RETRACTED; the W3 bisect falsified it three ways (static trace send_attach_resize -> serve_attach Resize arm -> brain.resize -> KIND_RESIZE -> dispatch_resize -> begin_resize_serialized; the synthetic sibling's pass; the broker's dynamic trace attach -> RESIZE_DISPATCH -> SYNC_PUSH{controller=true} with no RESIZE_SYNC_DROP) and then pinned the real root in the rc CLIENT's console mode (REQ-RC-NEWLINE-PRESENTATION-TRUTH / KNOWN-HAZARDS 7.56). The ADR-0047 Amendment 1 DECISION (geometry change = one transaction, one exit shape, any entry) stands on its own merits and is not disturbed. WHAT THIS REQ NOW OWNS — the captured-shape regression apparatus that the bisect proved worth keeping: hertz's ENLYZEAM production capture + its integrity pins (*.log -text, the fixture README sha256 table, per-record length refusal in both the player and the reader); the capture-player replay vehicle; and the probe ladder that makes any verdict from it mean something — vehicle transparency per CELL (ConPTY re-renders, so byte equality is unsatisfiable by construction), the pre-state soundness self-check (narrow-captured bytes into a wide authority is sound only if absolutely addressed), the capability probe re-run at every shape revision with its own precondition asserted, and the no-vehicle control (grid+resize+repaint over the same bytes = 0 divergent rows, the standing proof the seam is clean). Oracle clauses are binding for this rig class and are written out in Amendment 2. Gate: unit — the vehicle-transparency predicate + its non-vacuity sibling + the record-integrity refusals; int — the client-model rig pair (rc-fixed vehicle = clean, rc vehicle = reproduces the field scrap) plus the no-vehicle control. NO impl stage: there is no broker-side implementation owed, and pretending otherwise would point an activated stage at evidence that does not exist. Kin: REQ-RC-NEWLINE-PRESENTATION-TRUTH (the actual fix), REQ-RESIZE-INPUT-MODE-INTEGRITY (7.55, untouched — its capture has no replay vehicle in its path). [OK] REQ-ATTACH-SEED-REQUEST-DOUBLE-SERVE required: [] stages: -doc -impl -unit -int A dispatcher-SEEDED attach Request is honored ONCE — the wire replay's copy of that same Request must not re-establish it. (Seed: DAEMON-LIFECYCLE W1 build, answering ADR-0047 decision 3's banked open question; origin CODE-PROVEN, not inferred.) THE ORIGIN: serve_attach (spt-daemon/src/attach.rs) processes the dispatcher-pinned `seed_request` through handle_attach_request BEFORE its event loop (ADR-0038 B, the durable-opener seed — it exists because a ROLLED ring can no longer replay the Request). It then subscribes the stream from the floor and enters the loop, whose `AttachRecord::Request` arm calls handle_attach_request UNCONDITIONALLY — there is NO `!attached` guard, and NO record that the seed already established this exact Request. When the ring has NOT rolled, the wire replay still carries that Request, so the SAME record is handled TWICE on the SAME conn, milliseconds apart, with identical (session_id, from_seq, intent, gen, identity) — exactly hertz's v0.39.4 bug-4 capture (conn1011, 15ms apart, post-refresh replay). The two arms are the same function on the same seed record; the seed is pinned by the dispatcher on the post-cycle replay, which is the field trigger. SIBLINGS to check at triage: any other serve family that seeds an opener record AND re-reads it from the replay (xfer/rest seeds), and whether a re-established seat should also re-run the access check. FIX SHAPE (not built, W1 deliberately shipped the broker-side idempotence instead): the serve worker knows it already honored a Request — a guard on the loop's Request arm (already attached + same record = ignore), which fixes the ORIGIN rather than absorbing it at the broker. The broker idempotence stays regardless: it closes the symptom for EVERY origin, including origins not yet found. A serve-path int rig replaying the REAL double-serve shape belongs to this REQ, not to W1 (doyle ruling 2026-07-22: do not simulate a behavior we have not explained). Kin REQ-ATTACH-IDEMPOTENT-REPLAY (the broker-side absorber), REQ-STREAM-OPENER-DURABLE (why the seed exists), ADR-0038 B. [OK] REQ-BENCH-STEP-LEDGER required: [impl, unit] stages: -doc +impl +unit -int BUILD-STEP TIMINGS ARE RECORDED AS ONE JSONL ROW SHAPE, READ ON DEMAND, AND GATE NOTHING. Coverage is CI workflow steps AND the local mechanical steps agents invoke on this box (gate-rig prebuilds, wave gate lanes, assembly compile-gate, release packaging) -- the local gate builds are the largest measured wall-clock sink and are invisible to CI timing entirely. Instrumentation lives in the workflow and in xtask timing wraps; it is NOT an spt binary feature. ONE ROW SHAPE: step name, sha or input-key, host, duration, timestamp -- plus an ok flag, because an aggregate that blends a three-second failure with a twenty-minute success under one step name describes neither, so failures are excluded rather than averaged in. TWO WRITERS, ONE FORMAT: the local wrap is xtask, the CI wrap is a shell snippet (the xtask binary cannot time the build that produces it), and a parity test pins the snippet against the single field list, because a format that drifts silently makes the history unreadable at exactly the moment somebody wants a trend. THE LEDGER IS NEVER COMMITTED: CI emits a per-run artifact, local steps append to a gitignored directory; a hand-maintained ledger file in git is the conflict-magnet class every lane would touch. REDUNDANCY IS KEYED ON THE INPUT, never on the run count: two runs of one step at the SAME key is repeated work and two runs at DIFFERENT keys is just work, and a counter that cannot tell them apart flags every honest build as waste; a repeat is reported as a CANDIDATE, since a rerun after a fix is legitimate. WRAPPING IS TRANSPARENT: the wrapped command's stdio is inherited untouched and its exit code is propagated, so instrumenting a step cannot change what that step does or how a caller reads it. Consumption is `xtask bench-report` on demand, read at milestone close-out, findings become filings. NO dashboard and NO per-run regression gate: this box's wall-clock moves with whatever else is building on it, so a threshold would manufacture reds out of load. [OK] REQ-BIND-HONEST-SELF-STAMP required: [impl, unit, int] stages: -doc +impl +unit +int C2-ROOT (F028, doyle RCA 2026-07-03): the identity-attribution ROOT behind REQ-SESSIONS-LOG-ENDPOINT-ATTRIBUTION — three composing defects let a psyche-hosted SessionStart hook stamp a FOREIGN perch: (1) roster::detect_self_id leg (a) scans the owlery ONE level (roster.rs:107 read_dir(owlery)) so a NESTED psyche perch owlery//nested/-psyche can never self-resolve; (2) [session.psyche_init] (claude-spt manifest ~L347) spawns the psyche with NO env_remove + no pinned child identity, so whatever OWL_SESSION_ID/SPT_AGENT_ID reaches the child resolves to a foreign top-level perch; (3) the hook then writes info.json (session_id/pid rebind) + sessions.log on the mis-resolved victim. FIX (spt-core half): (i) identity PINNED at spawn — ManifestRuntime role spawns inject the child's OWN SPT_AGENT_ID= + OWL_SESSION_ID=, AND detect_self_id enumerates NESTED perches (fix the one-level owlery scan); (ii) BIND HONESTY — a session-start stamp may only write a perch whose resolved id AFFIRMATIVELY matches; never a fallback pick; refuse + loud-skip when unresolved (kin REQ-MSG-CLI-ORIGIN honest-default + #9 ancestry). ADAPTER half = PERRI touchpoint (psyche_init env scrubbing / relies on runtime pinning; hook loses silent fallback-perch behavior) — FLAG doyle BEFORE any manifest move (adapter glue-model rule). See triage C2 fix (i)+(ii). [OK] REQ-BIND-PSYCHE-CUSTODY-SQUAT-GUARD required: [impl, unit] stages: -doc +impl +unit -int MSG-IDENTITY W1 / F-036 leg c (perri field RCA, doyle ACCEPTED defense-in-depth): a bind whose --set-session-id equals a NESTED psyche perch's own custody sid is definitionally wrong and MUST be refused — core owns psyche-custody.json and can see the collision at bind time. ROOT CONTEXT: with the F-036 env leak, each stolen bind carried a psyche sid as the new pin; the identity-env sanitize (leg a) removes the known vector, this guard makes the CLASS unreachable (any future vector that tries to rotate a real endpoint's perch onto a psyche's custody sid is refused loud). Gate: a bind attempt whose target sid appears in psyche-custody.json as a psyche's OWN sid is REFUSED with a distinct loud token (unit: custody-sid collision refuses; a normal non-custody sid bind is unaffected). Kin REQ-PSYCHE-SID-CUSTODY, session-pin cluster, [[spt-core-findings-backlog]] F-036. [OK] REQ-BOOTSTRAP-FIREWALL-ENFORCEMENT-CODES required: [impl, unit] stages: -doc +impl +unit -int THE ENFORCEMENT VERDICT READS RAW NUMERIC EVIDENCE, NEVER DISPLAY SPELLINGS (releases#297 / #304 W2). Raw UInt16 EnforcementStatus 1 renders as Enforced on HFENDULEAM, not the documentation's Full; read CimInstanceProperties, not adapted values or coercions. AMENDED BY DOYLE 2026-09-14, run-02 scratch matrix: a Private+Domain rule without an explicit Package argument reports PrimaryStatus OK and EnforcementStatus [5,1] on this Private-only host; Package Any changes it to Inactive/[5,20]. Exact-[1] acceptance is superseded. ENFORCED iff raw PrimaryStatus is 1 (OK), EnforcementStatus contains 1, and EnforcementStatus does not contain 20 (NoLocalUser). Inactive-profile code 5 alongside success is not a refusal; profile scope is unchanged. Empty arrays, arrays without 1, any array containing 20, and non-OK PrimaryStatus refuse. Missing/null properties, null elements, non-numeric values, wrong CIM types and out-of-range transport remain distinguishable representation faults; never coerce them into success. This row owns transport and enforcement interpretation only: scope, hygiene, persistent source, ownership and LAN derivation remain the parent REQ-WEB-LAN-BOOTSTRAP-FIREWALL contract. Unit evidence includes OK/[5,1] and OK/[1] accepted, Inactive/[5,20] refused, code-20 veto even beside 1, non-OK status despite 1, and malformed numeric transport refused. [OK] REQ-BOOTSTRAP-FIREWALL-INVOCATION-LOG required: [impl, unit] stages: -doc +impl +unit -int EVERY HOST-FIREWALL CHILD INVOCATION IS TIMED AND ITS OUTCOME NAMED IN THE DAEMON LOG (releases#304 W2 rider item 1, doyle ruled 2026-09-12; F-A1-1's instrument). The field leg that found F-A1-1 could only learn WHICH leg spent the subprocess budget by reading the Windows event log by hand: the reconcile's writes were timestamped ~2.5-3.2 s before the command returned, so the rule pair already existed while the product still reported the admission UNVERIFIED. Each invocation logs its LEG (verify-query, reconcile-write, cleanup, is-clean, or the Linux tool name -- never just the program, since every Windows leg runs the same powershell.exe), its wall in milliseconds, and an OUTCOME distinguishing completed, failed and killed. KILLED IS NOT FAILED: the budget expiring says nothing about whether the host changed, which is the whole content of F-A1-1, and a try_wait error that also kills the child is reported as failed rather than dressed as a timeout. A WALL WITHOUT ITS OUTCOME IS NOT A MEASUREMENT (hertz's register line, same lane): a script that fails to parse produced the fastest walls of the field night, 209 ms against a real leg's ~2000 ms, so anything ranking legs by wall alone puts a run that executed no statement first. This requirement is the instrument that answers the unpartitioned bring-up term in the field, and it ships independently of any query-shape change. Gate: impl -- the timed wrapper around every invocation with the leg label and the three-way outcome; unit -- the outcome classifier asserted on all three arms, including that a killed invocation does not read as completed or failed. [OK] REQ-BOOTSTRAP-FIREWALL-SPELLING-EQUIVALENCE required: [impl, unit] stages: -doc +impl +unit -int THE ADMISSION VERDICT COMPARES WHAT A RULE ADMITS, NOT HOW THE HOST FIREWALL SPELLS IT -- AND EVERY OTHER DIFFERENCE STAYS REJECTED (releases#304 W2, doyle's repair contract 2026-09-12). THE ANTI-WIDENING HALF IS THE REQUIREMENT, not a caveat on it: this row exists to bound a normalization, and a normalization that admits one scope nobody chose is worse than the string comparison it replaced. Narrow to REQ-WEB-LAN-BOOTSTRAP-FIREWALL, which owns the admission pair and the scope policy this compares against; this row owns only HOW the two sides are compared. EQUIVALENCE, all three axes: an application filter that is ABSENT satisfies a spec wanting none however the host renders it (the empty string a program-less spec means, and the literal `Any` NetSecurity reads back), and NEVER satisfies a spec that wants a program; a PROFILE field compares as the SET of flag tokens it names, so one set in two renderings (`Domain, Private` against `Private,Domain`) is one set; an IPv4 NETWORK compares by value across prefix form (`192.168.1.0/24`, what the composer emits), mask form (`192.168.1.0/255.255.255.0`, what NetSecurity renders back) and a bare address, which is the /32 containing only itself. REJECTION, and each clause is a scope the policy did not choose: a genuinely NARROWER remote (a single host inside the derived prefix), a genuinely WIDER one (`Any`, `0.0.0.0/0`, a shorter prefix), a DIFFERENT network, and a profile SUBSET or SUPERSET -- `Any` is NEVER expanded into the named profiles, or an all-profile rule would satisfy the LAN half and admit the local subnet of a PUBLIC network, which is the exact widening the two halves exist to prevent. MALFORMED INPUT IS REJECTED FROM BOTH SIDES, and identical malformed text on the two sides is NOT a match: a prefix out of range, an absent or negative length, a non-contiguous mask, a truncated or out-of-range address and the empty string must answer `does not satisfy` rather than parse loosely, round to a nearby prefix, or panic -- and a SPEC whose own remotes are not networks is satisfied by NOTHING, including a rule carrying that same unparseable text, so equality between two literals can never stand in for a scope neither side can name. NORMALIZATION IS CONFINED TO THE SPEC MATCH: hygiene, source store and ActiveStore enforcement are decided at their own arms on fields these functions never see, and no canonicalization may make a rule that failed one of them satisfy a spec. BOTH SITES OF THE ONE DEFECT ARE GOVERNED: the verdict's comparison, and the lan-scope-moved log line, which on raw strings announces a move on every reconcile of an unchanged host and teaches the operator to ignore the line that exists to be noticed. Gate: impl -- the program/profile/network token normalizers, the wanted-side parse requirement, and their use at the spec match and the scope-change line; unit -- equivalence proven on each axis with each arm asserting the RAW strings differ first, so no cell can pass on a normalizer that does nothing, plus narrower, wider, different, subset, superset and malformed inputs staying unsatisfied against a positive control, and identical malformed text on both sides staying unsatisfied. [OK] REQ-BOOTSTRAP-FIREWALL-VERIFY-ONE-PASS required: [impl, unit] stages: -doc +impl +unit -int THE ADMISSION VERIFY CERTIFIES EFFECTIVE AND PERSISTENT FROM ONE EFFECTIVE-STORE PASS, AND A VERIFY THAT CANNOT COMPLETE DOES NOT WEAR A FAILED-WRITE FACE (releases#304 W2 rider item 2, doyle ruled 2026-09-12). The verify must certify BOTH that the pair ADMITS PACKETS NOW and that it SURVIVES A REBOOT; two store passes are one way to certify that and not the requirement. Each effective rule reports its own source store, and 'Local' there names the local persistent store as where it came from, so one ActiveStore pass carries both facts. The second pass cost a rule enumeration plus a complete seven-filter walk per rule, where the measured cost is ~85-90% PER CMDLET CALL rather than per rule returned (HFENDULEAM field leg 2026-09-12: a call returning 2 rules ~470 ms against ~550 ms for one returning ~1000; the walk's per-call class measured <= ~39 ms). A rule that is effective from a NON-LOCAL source is REFUSED BY NAME rather than reported unreconciled, because a Dynamic rule is indistinguishable from ours by name, port and scope and a group-policy name cannot be repaired by writing another local rule. CLAUSE (c): after the writes have landed, a verification that cannot reach a verdict -- the subprocess budget expiring mid-walk is the measured case, F-A1-1 -- is reported as WRITTEN-BUT-UNVERIFIED with rerun guidance, never as a refused write, because an operator reading 'reconcile failed' reaches for elevation and permissions while the rules are already in place. Gate: impl -- the single-pass query reporting each effective rule's source, the source judged in Rust, and the two distinct post-write faces; unit -- a pair whose source is not Local is refused and the refusal names the rule and the observed source, an empty effective store does not verify however the persistent store reads (doyle's red-on-purpose cell: present in PersistentStore, absent or overridden in ActiveStore must NOT verify), and the mismatch face and the written-but-unverified face are distinct with the latter stating the write landed. [OK] REQ-BOUNDARY-ROTATION-CREDENTIAL required: [doc] stages: +doc -impl -unit -int api boundary's rotation credential is designed, documented, and eventually anchor-proven (ADR-0032): the proof is the DEPARTED session's (prior sid / token) — --to-session-id is payload, never proof; the published surface documents the adapter prior-sid persistence pattern + loud-refusal requirement; the design-true end-state additionally accepts an OS-verified parent_pid-anchor ancestry proof making adapter sid-state optional [OK] REQ-BRAIN-HASH-ONCE required: [impl, unit] stages: -doc +impl +unit -int REGISTRY-LIFECYCLE W1 (ADR-0040 rider; dropped THE-FORKENING W4 rider escalated — hertz re-measured live 2026-07-17: 61.29 MiB/s predicted vs 63.91 observed, 15.5%/core): the brain executable self-hash is captured EXACTLY ONCE per brain process (OnceLock in run_brain before the heartbeat loop); every write_ready reuses the cached value; failed capture stays None with no per-tick retry; current_exe_hash doc-comment corrected same commit. Once-at-start capture IS the resident-bytes truthfulness contract: the per-tick PATH re-read published the NEW file hash from a resident-OLD-bytes brain post-swap (breadcrumb lie in the enlyzeam class it exists to catch). 500ms ready-write cadence unchanged. Gate: impl — cached capture; unit — injected digest-counter==1 across initial+N heartbeat publishes with pid/generation/hash stable, fresh process fixture computes independently; existing D7 process-replacement e2e retained green (new brain publishes new hash first write). [OK] REQ-BRAIN-READ-BOUNDED-PER-CALL required: [doc, impl, int, unit] stages: +doc +impl +unit +int Every ordinary Brain RPC reply-wait loop shares ONE fixed deadline computed after its send and before its loop; unrelated frames cannot renew the call budget. WAN stream reply-waits (crates/spt-daemon/src/wan.rs, six sites) are a SEPARATE contract, REQ-WAN-REPLY-BOUND: a progress policy whose budget is renewed only by data on the matching stream, so unrelated frames renew neither kind of wait. The unbounded read_event convenience API is removed and all callers use read_event_until with an explicit deadline. Whole carriers retain their existing None budget. STREAM LOOPS (serve_*_feed, the attach serve loop) are not reply-waits and never take a per-call budget: their deadline is an explicit None, and any future silence bound on them is per read, never per call. The six WAN reply-waits are excluded from the static fixed-deadline audit by record; their renewal behaviour is not certified by that audit. [OK] REQ-BRAIN-READY-WINDOW-OBSERVABLE required: [impl, unit] stages: -doc +impl +unit -int THE BRAIN_UP -> brain.ready WINDOW IS OBSERVABLE IN ONE GREP (RCA-242-R2-LINUX, ruled 2026-08-29). Every happy-path stretch between the BRAIN_UP line and the ready file used to be SILENT: announce_coordinator_image's Ok(true) arm is empty, resume_session_cursors' Ok(_) arm is empty AND is the always-taken one (the supervised daemon brain hosts no PTY sessions), and write_ready's first current_exe_hash() reads and SHA-256s the whole binary inline before the atomic write. THE COST WAS PAID TWICE: four golden cells burned a full untuned 30s readiness budget while their own daemon stderr showed BRAIN_UP at 26-65ms, and the stall point -- blocked on a broker reply READ versus inside the exe-hash -- could not be separated from the log at all, so the RCA closed UNDISCRIMINATED by construction. activity_link_push_e2e then failed again WARM, which carved it out of the load-tail ruling and made the undiscriminated window gate-blocking rather than a footnote. THE SHAPE IS ONE TOKEN FAMILY WITH PER-PHASE ELAPSED MS: three BRAIN_PHASE lines whose figures SUM TO THE GAP, so a reader names the stretch in one read instead of a second instrumented run. ELAPSED, NEVER WALL STAMPS -- CONN_LIFECYCLE already carries wall_ms for correlation and a second clock in one log is how two clocks start disagreeing. THE READY LINE ALSO CARRIES THE RESOLVED PATH (gate rider): a test polls a path it resolved itself, so a poller/writer mismatch is ruled in or out rather than inferred. THE HASH FIGURE IS TIMED WHERE IT IS PAID, INSIDE write_ready, AND CARRIES computed=true|false. This is the requirement, not an implementation detail: current_exe_hash memoises in a OnceLock, so an instrument that timed a SECOND call from the caller would measure a cache hit and report ~0ms with computed=false on every run -- a vacuous meter for the one stretch it exists to measure, and the failure would be invisible because 0ms reads as good news. EMISSION IS emit_line_err! THROUGHOUT so these breadcrumbs never become the next census's residue. SCOPE BOUNDARY, RULED: moving current_exe_hash OFF the ready path is the honest fix for that stretch and is NOT part of this requirement -- it touches ADR-0018 Q7's ratified readiness semantic and REQ-BRAIN-HASH-ONCE's resident-bytes truthfulness contract, and rides its own board request. [OK] REQ-BRAIN-RESUME-NO-CONN-DEADLOCK required: [doc, impl, int] stages: +doc +impl -unit +int UPDATE-WEDGE round 3 (v0.30.5, doyle-ruled Option A 2026-07-09 — the v0.30.4 field-verify re-wedge, root code-PROVEN + dead-peer-INDEPENDENT): the daemon brain must NOT subscribe broker PTY sessions onto its own request/reply IPC conn — it has no consumer for that output and the subscription DEADLOCKS the conn. ROOT (todlando code-read, docs/UPDATE-WEDGE-2-ROUND3-CODEREAD.md; the net-runtime AND the counter-54 reap-drive were both FALSIFIED first — docs/UPDATE-WEDGE-2-ROUND3-RIG-VERDICT.md): a conn's send half is a single `SharedSend = Arc>` (broker.rs:78). Subscriber writer threads (`viewer_writer` broker.rs:1333/1342, `controller_writer` :1451) hold `send.lock()` ACROSS a BLOCKING `write_frame`; the dispatch reply path (`send_frame` :4221 → KIND_SESSIONS_REPLY / KIND_NET_STATUS_REPLY) needs the SAME lock. `resume_sessions` (brain.rs:1031→1054) subscribes every session as a Viewer onto the brain's MAIN conn — which is ALSO the brain's request/reply channel. The daemon brain hosts no PTY sessions (brainproc.rs:184) so run_brain never drains that output; it reads the conn only during the 500ms-heartbeat net_status()/sessions() calls (drain-and-DISCARD, `_ => continue`). When an actively-streaming session backs the conn up, a subscriber writer BLOCKS in write_frame holding send.lock() → the dispatch thread can't send the heartbeat reply → net_status()/sessions() never return → the heartbeat loop stalls → the brain never drains → the writer stays blocked = SELF-DEADLOCK on the brain conn's send mutex → every subscriber writer on it wedges → BRAIN_SUBSCRIBER_STALL_EVICT (controller writer blocked >15s). BOTH severities, one mechanism: RESPAWN (resume subscribes N + the replay burst floods the conn before the loop drains) AND STEADY-STATE (an active streamer's output between heartbeats fills the socket buffer). Counter-54 (REQ-UPDATE-TRIAL-DRAIN-DRIVE) added a 2nd per-heartbeat reply round-trip (sessions()) through the contended mutex — WIDENED the window (regression-window-exact), did not create it. The round-2 Viewer fix (REQ-BRAIN-RESUME-NO-CONTROL-STEAL) removed the STEAL but kept the brain a SUBSCRIBER — viewer_writer has the same send.lock()-across-write pattern AND viewers have no stall-evict valve — so v0.30.4 field-verify wedged again. FIX (Option A, brain-side, SEAMLESS): resume_sessions does NOT subscribe (drop the subscribe_with call; §3 verification guard confirmed NO brain consumer — digest/relay/net-consumer/shellwake/presence — reads the resumed subs). The brain conn then carries only request/reply → no subscriber backpressure → no deadlock. Keep the session_cursors seed only if harmless. Rides the brain-swap ⇒ seamless (no daemon.rs:368 broker restart). Option B (a dedicated Split-reader drain of the brain conn, brain.rs:230 — the pump's carrier) is the FORWARD path for when genuinely daemon-DRIVEN sessions land (the live-agent adapter) — deferred, noted, not built. Option C (the broker-side durable CLASS fix) = REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK, deferred. Int = the confirmer rig (brain+broker+PTY, brain_decouple template): N sessions actively producing output, brain resume-subscribed onto its req/reply conn; RED-first = heartbeat stalls + an active-streaming controller stall-evicted >15s WITHOUT any dead peer present (proves dead-peer-independence); assert BOTH severities (respawn interleave + steady-state output backup); Option A turns both green. Composes with REQ-BRAIN-RESUME-NO-CONTROL-STEAL (the round-2 Viewer fix this supersedes as the wedge cure), REQ-UPDATE-TRIAL-DRAIN-DRIVE (the counter-54 reap-drive that widened the window), REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict it stops triggering). [OK] REQ-BRAIN-RESUME-NO-CONTROL-STEAL required: [doc, impl, int] stages: +doc +impl -unit +int UPDATE-WEDGE round 2 (v0.30.4, doyle-ruled 2026-07-09 — field incident on the counter-54 fetch--apply): a brain-respawn must NEVER steal, then stall-evict, the controller of a broker PTY session the daemon brain does not DRIVE. ROOT (field-pinned + SME, docs/UPDATE-WEDGE-2-RCA.md + docs/UPDATE-WEDGE-2-SME-todlando.md): `resume_sessions` (brain.rs:985) re-attaches EVERY session `KIND_SESSIONS` returns via `subscribe` = `subscribe_with(AttachIntent::Control, by:None)` (brain.rs:1450-1455). The docstring's 'a None identity never displaces (falls back to viewer)' is a MYTH for FREE / SAME-LOCAL-IDENTITY slots: `resolve_subscribe` (broker.rs:1134-1153) stall-evicts FIRST, then `become_controller` if the slot is now free OR the incumbent is also local (None==None) — viewer-fallback fires ONLY when a DIFFERENT REMOTE controls. So on a box with N spt-hosted broker PTYs, a brain-respawn STEALS the by:None controller of every free/local-controlled session (incl. the operator's LOCAL `spt rc`), which the daemon brain never drains (it hosts no PTY sessions — brainproc.rs:184) → 15s later `stall_evict_controller` (broker.rs:1039) releases driven_by → the session is UNCONTROLLABLE (Failure A). It also head-of-line-blocks the shared brain↔broker conn on the N-session controller-replay burst → every journaled `spt rc` retake deadlines ('brain IPC read deadline', the REQ-BROKER-ATTACH-JOURNAL-RESILIENT / #16 shared-conn symptom) → global rc failure on ALL N (Failure B). Field 2026-07-09 (operator-confirmed): fetch--apply 0.30.2→0.30.3 PROMOTED CLEANLY (the counter-54 fix worked) but under 7 spt-hosted PTYs (ALL with LOCAL by:None controllers) the resume SILENTLY STOLE all 7 (become_controller same-local re-take, NO Displaced notice → orphaned, output froze immediately, no rc-detach splash) + blocked every rc retake. The 5-vs-2 stall-evict split is ACTIVE-vs-IDLE, not remote-vs-local: 5 producing output → stolen writer blocked >15s → stall-evict; 2 idle → writer parked → no evict, but still silently stolen+frozen. The counter-54 promotion fix did NOT cause this — pre-existing resume-steal latent bug, hidden until N broker PTYs were present at a respawn; the single-black-holed-session A'-rig never exercised N-live-controllers-under-replay. FIX (brain-side): `resume_sessions` re-attaches as **Viewer** (`AttachIntent::Viewer`), NOT Control — a viewer never touches driven_by and is never stall-evicted (broker.rs:1063+ bounded try_send + private eviction), so steal-then-drop vanishes and the operator keeps/regains control; and it relieves shared-conn pressure (a slow viewer is DROPPED, never a 15s controller block) so rc retake gets through. Control ONLY for sessions the daemon brain genuinely DRIVES (empty set today → all become Viewer; forward-correct for the live-agent-adapter future). SECONDARY (escalation, ONLY if the gate shows residual B): stagger the resume re-attach + bound the viewer replay so the respawn burst can't saturate the conn. Int = the multi-broker-PTY-session RESPAWN rig (the coverage the A'-rig lacked): N real broker-spawned sessions with controllers producing output → real brain respawn/promote → assert (1) EVERY session keeps its controller across the swap (no stall-evict of a session the brain doesn't drive), (2) `spt rc` attaches/retakes IMMEDIATELY post-promote (no shared-conn saturation), (3) promotion still succeeds. RED-first: the current Control re-attach steals+evicts + deadlines rc. Composes with REQ-UPDATE-TRIAL-DRAIN-DRIVE (the orthogonal counter-54 promote fix), REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict it stops mis-firing on non-driven sessions), REQ-BROKER-ATTACH-JOURNAL-RESILIENT (the #16 shared-conn resilience). Distinct from REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE (that = the OLD brain's outgoing black-hole; this = the NEW brain's resume-steal). [OK] REQ-BRAIN-UPDATE-RESTART-CLEAN-CLOSE required: [] stages: -doc -impl -unit -int SEED (DEFERRED, doyle 2026-07-09 — post-counter-54 root-hardening for UPDATE-WEDGE; mint now, impl a FUTURE milestone): on a PLANNED brain-restart (`BRAIN_UPDATE_RESTART`, the seamless update-apply brain-cycle), the outgoing brain's LOCAL (by:None) controller conns are GRACEFULLY CLEAN-CLOSED as the brain is cycled, instead of hard-killed and left to black-hole. ROOT (field-pinned 2026-07-09, daemon.stderr.log L24277-24303): the update-restart path hard-kills the outgoing brain (`child.kill()`, brainproc.rs:851); its live-agent controller conns then block on dead pipes (never EOF) → the broker reads them WEDGED (broker.rs:2695-2700) → the new candidate's promotion DRAINED gate (`any_local_controller_wedged`, broker.rs:2704) stays true until the W2 stall-evict matures (~15s). REQ-UPDATE-TRIAL-DRAIN-DRIVE (counter-54) makes the candidate DRIVE that reap so it promotes within the 30s window — but at a ~15s wedge-maturity hitch (frozen PTYs during the swap). A CLEAN close makes the conn 'simply absent → drained=false AT ONCE → fast promote' (broker.rs:2699-2700), ELIMINATING the hitch = truly seamless (honors the paradigm the field freeze broke). SUPERSEDES the earlier livehost-reattach framing of 'Fix Y': livehost is SPAWN-FRESH (fresh session uuid/pid per boot, nothing to re-attach — wrong site, and it never ran in the trial window); the correct site is the brain-cycle / update-restart path (a bounded graceful-drain of the outgoing brain BEFORE the kill). Non-trivial: hard-kill → bounded graceful drain; a drain that hangs must NOT wedge the swap (timeout then kill anyway, never block the update). Composes with REQ-UPDATE-TRIAL-DRAIN-DRIVE (defense-in-depth reap-drive REMAINS for any conn that still black-holes — a peer/relay conn, a drain-timeout kill) + REQ-UPDATE-PROMOTE-DRAINED (the gate) + REQ-HAZARD-BROKER-VIEWER-BRAIN-DECOUPLE (the stall-evict). Int: a planned update-restart under a live-agent controller conn → the new candidate promotes WITHOUT waiting the ~15s wedge-maturity (drained reads false immediately, no STALL_EVICT), RED-first vs the current hard-kill-then-reap-drive ~15s hitch. [OK] REQ-BROKER-ATTACH-JOURNAL-RESILIENT required: [impl, unit] stages: -doc +impl +unit -int A poisoned EffectJournal mutex or a sick NetHost runtime must NOT permanently brick all future attaches. Bug #16 (URGENT): a live spt-hosted endpoint (eel-a) attach fails with 'brain IPC read deadline elapsed' after a self-update brain-respawn — the broker survives the respawn and one journaled op (dispatch_net_stream_open journal.apply_once + loopback open_stream runtime.block_on nethost.rs:1060) enters a bad state, so every journaled attach silently kills its per-conn reply thread while non-journaled ops keep working. Fix: recover PoisonError via into_inner (effect.rs apply_once, replace the .expect panics) so one panic cannot brick all attaches; bound the loopback open_stream block_on (nethost.rs:1060) like the QUIC bounded_block_on so a sick runtime fails fast with an error frame not an opaque 10s deadline. Reinforces REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #16. [OK] REQ-BROKER-EXIT-EVERY-REAP required: [impl, int] stages: -doc +impl -unit +int PORTER (releases#201, todlando mechanism pass 2026-08-21): the broker exit waiter emits a terminal KIND_EXIT for EVERY reap it performs, INCLUDING the reap whose PtySession::wait() yields Err. This requirement states EXISTENCE; REQ-BROKER-OUTPUT-BEFORE-EXIT states ORDERING — the two are separate seams over the same frame, and any Exit this requirement adds is bound by that one's exit-behind-output enqueue discipline. TODAY broker.rs's `if let Ok(status) = status` gates the WHOLE Exit emission block, and only that block: the row removal, the best-effort kill, the drain bytes_forwarded quiesce and the reap-time unlatch (stamp_reaped, REQ-HAZARD-CONTROL-STAMP-LIFETIME) all sit OUTSIDE it and all still run on the Err arm (measured at b88fab2a: block spans broker.rs 6430-6457, stamp_reaped at 6467). So the session is genuinely, completely gone from the table AND the perch is unlatched, while NO Exit frame ever reaches the controller or viewer sinks — the attached rc has no terminal event to act on and burns its full 30s reconnect window over a session that can never come back. The Err arm is reachable in the field, not only under injection: PtySession::wait (spt-term/src/pty.rs:203) is `self.child.lock().unwrap().wait().map_err(SurfaceError::Io)`, so ANY Io error on the child handle takes it — the kill-vs-natural reap race included, where our own kill() and the child's natural exit contend for the same handle. AN ABSENT EXIT CODE IS NOT A ZERO AND IT IS NOT A NO-RECORD (the absent-is-not-zero discipline, releases#197): ExitEvent.code is ALREADY Option, so the Err arm can name the death with the code ABSENT rather than fabricating a 0 or suppressing the frame — a suppressed frame is indistinguishable to the viewer from a session still running. Gate: impl — Exit enqueued on BOTH arms of the wait outcome, code absent-but-named on Err, still behind the drain quiesce; unit — the event built for an Err reap carries code: None and is not skipped; int — a NARROW fault valve makes wait() yield Err while row removal, drain quiesce and stamp_reaped ALL still run (the existing SPT_TEST_EXIT_WAITER_SUPPRESSED is precedent for the shape but suppresses the ENTIRE waiter, so it does NOT reproduce this face) => the attached controller and viewer OBSERVE an Exit frame, with the valve-off control arm proving the same panel observes the healthy Exit (non-vacuity: a panel no assertion depends on is untested by construction); doc — KNOWN-HAZARDS. [OK] REQ-BROKER-OUTPUT-BEFORE-EXIT required: [doc, impl, unit, int] stages: +doc +impl +unit +int RC-RENDER-TRUTH W3 (ADR-0043 decision 1, hertz stale-glyphs RCA leg 1 P0): the PTY drain/output writer is the SOLE FIFO sequencer for terminal Output + Exit per attach sink — Exit is enqueued BEHIND all prior output (drain EOF/completion first, then Exit); the exit waiter never direct-writes KIND_EXIT around the queued output path (a mutex serializes bytes, not producer order). Kills the stranded-final-frame race (final EL/SGR-reset/cursor-show/?1049l lost when Exit overtakes Output — already admitted and compensated in the broker test suite, never fixed in production rc). Gate: impl — single sequencer, exit-behind-output enqueue; unit — ordering invariant on the writer queue (exit never precedes queued output for a sink); int — short-lived child emits 'XXXX ESC[2K ESC[?25h ESC[?1049l' then exits => that exact Output precedes Exit through the PRODUCTION broker->attach->rc path; doc — ADR-0043. [OK] REQ-BROKER-SCREEN-GRID required: [doc, impl, unit, int] stages: +doc +impl +unit +int Bugs #6 + #12 + #7/#8-artifacts: the broker is a raw-byte pump with no screen model — OutputLog replays the raw ring from seq 0 into a fresh terminal on attach, so an alt-screen TUI (Claude Code) corrupts scrollback (#6) and rc-to-a-pre-running-endpoint garbles (#12 — rc and endpoint run --attach are the SAME client fn, so it is replay content not a client-VT bug). Fix: a server-side VT/grid/screen model (tmux/mosh-style) that maintains authoritative screen + alt/main + cursor and synthesizes a CLEAN current-screen repaint on attach instead of replaying mid-stream ring bytes. Also eliminates residual-cell artifacts on animate/scroll/resize (#7/#8). Operator NON-NEGOTIABLE: accurate PTY representation with zero artifacts. (win32 vterm in the report means this server-side emulator, not ConPTY which is already the backend.) See docs/NEXT-MILESTONE-BUG-TRIAGE.md #6/#12. [OK] REQ-BROKER-STOP-DENY-NAMES-BLAST required: [impl, unit] stages: -doc +impl +unit -int The endpoint-deny refusal TELLS THE CALLER WHAT IT COST THEM AND HOW TO PROCEED, carrying TWO required elements rather than a bare denial. FIRST, THE BLAST RADIUS BY NAME: the refusal states that the operation would kill the running sessions of all spt-hosted endpoints and LISTS THE RESIDENT SESSIONS, reusing the composition the live-session refusal already prints — a refusal that says only 'denied' teaches nothing, while one that names the eight endpoints about to die explains itself. SECOND, THE GROUND AND ITS CURE: the refusal names WHICH ground identified the caller as an endpoint, and the ONE action that clears THAT ground. THE PRINTED ACTION MUST BE TRUE FOR THE GROUND THAT ACTUALLY FIRED — an environment-variable ground prints unsetting that named variable, an ancestry ground prints running from a shell not descended from an agent session — because an instruction that cannot clear the condition it is offered for is the same defect as a warning that prints a command the reader cannot run, arriving from the opposite direction. THE REASON IT IS SAFE TO PRINT A CLEARING ACTION AT ALL is that clearing the environment does NOT clear the ground: an agent that unsets its identity variables is STILL caught by the ancestry leg, so the printed line helps a HUMAN whose shell merely inherited the variable while doing nothing for an agent trying to escape. That property is load-bearing and therefore PINNED BY ITS OWN UNIT — environment variables cleared plus an ancestry ground still refuses. Without that pin the printed unset line IS an escape hatch and this requirement inverts from a guard into a bypass manual, which is exactly the failure mode a prohibition written as text rather than enforced at the tool layer always reaches. Gate: impl — the two-element refusal composition with the per-ground clearing action selected from a TYPED ground rather than by matching the ground's prose, so the two can never disagree about which leg fired; unit — the refusal carries BOTH elements (the resident list AND the ground with its clearing action), each ground kind yields the action that actually clears it, and the escape-hatch pin: env cleared + ancestry ground still refuses. [OK] REQ-BROKER-STOP-ENDPOINT-DENY required: [impl, unit, int] stages: -doc +impl +unit +int A broker-stopping command invoked from a context that resolves to an spt ENDPOINT is a FULL NO-OP that refuses, under every flag combination. Two commands are broker-stopping: `spt daemon stop` (INCLUDING `--force`) and `spt update apply --finish`. THE GROUNDING IS THAT THE CALLER CANNOT SURVIVE ITS OWN COMMAND: stopping the broker kills the hosted PTY of every spt-hosted endpoint on the node INCLUDING THE INVOKER'S OWN, so the agent's next chained command dies with the session that would have reported the outcome — the failure is silent from the only side that could see it, which is why this is enforced at the tool layer and not written as advice an agent is asked to follow. THE HUMAN CONTRACT IS UNTOUCHED: the pre-existing live-session refusal keeps its exact shipped behavior and `--force` remains a human's override of it. The endpoint deny is deliberately NOT overridable by any flag, because the caller who would reach for `--force` here is precisely the caller who cannot live through the result; an override would be a footgun with a safety catch that only the wrong hand can reach. THE RESOLUTION SEAM IS REUSED, NEVER RE-INVENTED: the same agent-ground resolution the engine-room ceremony refusal already uses answers this question, so 'what counts as an agent' has ONE definition on this node and a change to it can never make two refusals disagree. REFUSAL PRECEDES EVERY SIDE EFFECT — before the live-session guard, before the stop-inhibit is minted, before any teardown — so a refused call leaves nothing behind to clean up. ONE GUARD SITE PER COMMAND FAMILY, and for the finish command that site is INSIDE the apply function's finish arm, which is what BOTH the explicit `update apply --finish` verb AND the composite update's lethal-last Finish leg route through: guarding the verb alone would leave the composite lethal, and guarding both separately would be two rules free to drift. The composite already treats a failed finish leg as non-aborting, so the refusal degrades into the existing contract instead of inventing one. THE NON-FINISH `apply` ARM STAYS UNGUARDED and that is a decision, not an omission: it hands off to the running broker in place, the broker survives, the invoker's PTY does not die, and guarding it would deny agents a leg that is safe for them today. Gate: impl — the pure deny decision over the reused agent-ground resolution, wired at both sites ahead of every side effect, with the single-site rationale stated at the finish guard so no one later adds a second one at the verb; unit — an endpoint context is refused with force=true and force=false alike, a human context is unaffected in both, the finish arm refuses while the non-finish arm proceeds, and the decision is pure over its ground input; int — an e2e proves an endpoint-context caller cannot stop the daemon by ANY flag combination and that the daemon is still running afterwards. [OK] REQ-BROKER-ZOMBIE-IDENTITY required: [doc, impl, unit] stages: +doc +impl +unit -int Broker zombie reaping checks the session root's spawn-time process identity before tree-kill; unknown identity declines without teardown, while a provably dead root or different observed birth permits stale-session cleanup and respawn without signaling its remembered PID [OK] REQ-BUILD-UNIQUE-BIN-NAMES required: [impl, unit] stages: -doc +impl +unit -int No two workspace packages declare a bin of the same name. Same-named bins share ONE `target//` output filename, clobber each other, and a CARGO_BIN_EXE_* lookup then resolves to whichever built last (the PR#28 Windows gate-escape). The invariant was previously carried ONLY as prose that named a single counterpart by name, and prose of that shape ages the moment the counterpart moves: a reader greps the named bin, finds nothing, and concludes the hazard is retired. ENUMERATION IS BINDING: the check reads cargo metadata target list, NEVER `[[bin]]` manifest stanzas — a stanza grep is structurally unable to see an autodiscovered src/bin/*.rs target, so it would certify an absence it cannot observe, which is how a live bin was recorded as non-existent in the register. [OK] REQ-CARRIER-CLAIM-EXCLUSIVE required: [impl, unit, int] stages: -doc +impl +unit +int MSG-IDENTITY W4 / F-033 + operator self-send probe 2026-07-09 (dup-delivery cluster, RCA-FIRST): a spooled message row is delivered by EXACTLY ONE carrier — the first carrier to take a row (hook-poll drain, relay idle-inject, relay-backlog, psyche) atomically CLAIMS it so no other carrier can re-deliver the same row. FIELD EVIDENCE (authoritative, operator-observed): a default-window doyle-to-doyle send while doyle was BUSY delivered on BOTH the busy POLL path AND the idle RELAY path; spool row 156: window='default', delivered=1, taken_leg='idle-inject' — the relay claimed a row a poll also surfaced (REQ-SPOOL-TAKE-AUDIT instrument, already shipped, is the RCA tool: taken_leg/taken_sid/taken_at per row). PRIOR: F-033 (perri 2026-07-08) — the psyche-download filing arrived as TWO copies, dup-delivery live-confirmed. RCA-FIRST (report-before-fix): pin whether the poll drains before/after the relay's delivered=1 mark; whether the busy-to-idle edge re-offers a row a poll already took; whether take-marking is atomic per carrier or check-then-mark racy. DISTINCT from F-035 (that = active_only window honor, spt-core exonerated; THIS = a default msg on both carriers — F-035's lock never asserted a default msg can't ride both). Gate: int — a default send to a BUSY live agent that polls mid-turn AND transitions idle delivers EXACTLY ONCE (spool-audit shows one taken_leg, recipient sees one copy); unit — concurrent take attempts on one row yield one winner. Kin REQ-SPOOL-TAKE-AUDIT, REQ-RELAY-NO-BUSY-DELIVER, REQ-IDLE-PARKED-DELIVERY, [[spt-core-findings-backlog]]. [OK] REQ-CI-DOCS-ONLY-THIN required: [impl] stages: -doc +impl -unit -int CI (operator-ruled 2026-07-16, from PR #8 review; registry-only extension operator-approved 2026-07-27; superseded for main pushes by ADR-0050 on 2026-07-29): a PR whose ENTIRE diff is Markdown OUTSIDE docs-site/ and/or the declarative `traceable-reqs.toml` registry runs THIN CI — the heavy build/test jobs are skipped via a changed-files classifier job. `traceable-reqs.toml` has no Rust/product consumer; its load-bearing correctness gate is traceability. Invariants: the traceability gate ALWAYS runs (doc/registry tags and activated-stage coverage are load-bearing evidence, and a run with zero checks is unmergeable); any docs-site/ change runs the PR lint/unit/traceability lane; any other non-Markdown path runs that same lane. Main pushes also run thin CI only; the full evidence for main's exact SHA is the uncancelled golden run that produced it, and main advances only by fast-forward to that tested SHA. The classifier is plain git diff over the PR merge commit (HEAD^1..HEAD, fetch-depth 2) — no third-party changed-files action on the self-hosted runners. Skipped-required-check note: GitHub treats an if-skipped job as satisfying required status checks, and the classifier + traceability always report, so thin PRs stay mergeable. [OK] REQ-CI-FREE-SPACE-PREFLIGHT required: [int] stages: +doc -impl -unit +int Every CI job on a shared self-hosted runner FAILS LOUD AND FIRST on insufficient free disk, never obscurely on a downstream timing/space symptom: a preflight step asserts a free-space floor on the workspace drive and fails naming the RESOURCE (free bytes, floor, drive) so the next exhaustion costs one clear red instead of a release window of mystery flakes (proposer: deployah, during the v0.44.0 disk-exhaustion incident — the rule-failed-its-author-needs-a-mechanism shape: 'check disk first' was already a memory rule and still cost two CI windows because it lived in judgement, not mechanism). Floor calibration honest at mint: the observed incident burned tens of GB per suite run (leaked fixture dirs + build targets); pick a floor from measured suite consumption plus headroom and RECORD the derivation in the workflow comment — an unexplained magic number invites drift. AMENDED 2026-08-03 (hertz RCA of golden 30860770146, doyle-ruled option (i) the same day): 'FIRST' now means first among EXPENSIVE steps, not literally step 1, in the two two-host jobs — there the floor is asserted AFTER `actions/checkout`, because checkout runs `git clean -ffdx` and that IS the reclaim. Measured: the Windows test leg materialises ~50 GiB of gitignored `target/` (29,777 of 30,022 files written inside that job's own window), the next job's checkout deletes it, and twohost-a starts seconds later — so the pre-checkout read is taken at the disk's maximum. 30860770146 died exactly there: RESOURCE red at 24.4 GiB free, no test run, over bytes its own next step was about to reclaim; the reorder alone would have turned that red green with zero reclaim. The protective purpose is intact — checkout is seconds and every build is still downstream — but the ordering is now a stated exception rather than an accident. EXTENDED 2026-08-29 to the n1-gate job (deployah, on the v0.66.0 SEMAPHORE cut; doyle informed with the measurement): n1-gate had kept the literal-first shape and hit the identical trap twice in one cut. Measured on golden 33273378238 attempt 2, hfenduleam: n1-gate asserted PRE-checkout at 21:58:36 and read 32,219,230,208 free against the 34,359,738,368 floor and failed in 9 seconds with checkout SKIPPED, while twohost-a on the SAME BOX asserted POST-checkout at 21:59:15 and read 104,530,997,248 and passed — 39 seconds apart, ~72 GB reclaimed by the intervening `git clean -ffdx`, opposite outcomes decided solely by read order. Two pre-checkout low-water readings 64 minutes apart (r2 20:54:36, r3 21:58:36) agree within 10 MB, so the adjacency is deterministic, not a flake, and a bounded WAIT placed before checkout cannot fix it by construction — the reclaim it would wait for is performed by its own next step. WIDENED the same day to the `test` job (doyle ruling on the scope note this entry carried): that latent-shape note is DISCHARGED, not deleted — leaving the amendment clause armed on one job is precisely how the trap survived 26 days to cost a cut two golden attempts. Gate applied before moving it: the `test` floor was verified to carry NO checkout-protective purpose — its comment derives the 32 GiB from the observed full-suite footprint plus headroom for one complete run, so it guards the SUITE, not checkout. The checkout-protective steps in that job are the two reap guards (Windows exe-lock, Linux flood orphans), whose own comments state they must precede checkout because the post-checkout guard is too late for the clean; they STAY pre-checkout and only the floor moved. EXTENDED 2026-09-06 (WEBSERVE floor trio IR-46/IR-59/IR-73): all eight ci.yml/release.yml floor sites are post-checkout, having no checkout-protective purpose. START and always-run END DISK assertions record raw free bytes, the unchanged 32 GiB floor, job/runner identity and PASS or RED, labelled INSTANT; END runs before teardown even after workload failure. Golden test and n1-gate carry the same readings, with a fresh assertion immediately before docs-drift. These samples are not a run minimum or a headroom guarantee. Raising the floor lacks measured peak evidence; continuous sampling is not part of this remedy. [OK] REQ-CI-LINK-HEALTH-PROBE required: [doc, impl] stages: +doc +impl -unit -int The shared-runner quiet predicate gains a NETWORK axis: both two-host jobs measure the link to their peer immediately before rendezvous, print it, and carry it in the bench ledger. (INFRA-REGISTER IR-1; hertz 2026-08-03.) WHY THE EXISTING PREDICATE CANNOT SEE THIS: its two axes are process-shaped (zero non-terminal CI runs; no local cargo/rustc/nextest by parent chain), and both PASSED on run 30771155390 — a run whose link took 321s for a 1s checkout with bidirectional 10s QUIC dial timeouts, PUMP_PEER_FAIL moving a 0→3→0 and b 8→22→8 across green/red/rerun. A network-shaped red was triaged as a product one because nothing in the run recorded the network. kitsubito is wifi-only INDEFINITELY (operator-confirmed 2026-08-02), so the link cannot be hardened and the predicate must SEE it. JITTER IS THE SIGNAL, NOT THE MEDIAN: the motivating link mostly worked, so the probe takes 5 samples and records BOTH `link-rtt-med-` and `link-rtt-max-` rows — measured while building this, an idle link ran 10..83ms on one direction and 3..75ms on the other, i.e. the spread is the whole datum and a median alone reads healthy through the failure. ALSO CARRIED: DERP-vs-direct path (a relayed rendezvous is a different network than the one the rig's timings were tuned on), sample loss, and the peer's resolved IP so a reader can check it against the rig's own SPT_TWO_HOST_PEER_IP. AN INSTRUMENT, NOT A GATE — always exits 0, no threshold refuses a rendezvous: whether a number should is an unmade ruling, and a probe that reds a run converts 'the link was slow' into 'the suite failed', which is the confusion it exists to remove. A FAILED PROBE IS A LABELLED HOLE, NEVER SILENCE: no tailscale on PATH or no reply still writes an `ok:false` row, because 'attempted and did not land' is a different claim from 'never probed'. The absence of the CLI is TESTED rather than caught on the pwsh side (under GitHub's `stop` preference a missing command is terminating), and the bash side uses no `| head` (pipefail + SIGPIPE = 141 on an instrument). Gate: impl = .github/bench/link-probe.{sh,ps1} + the probe step in twohost-a (peer kitsubito) and twohost-b (peer hfenduleam), both exercised on the REAL boxes before shipping — five-sample success, no-reply, and absent-CLI arms on each shell, the absent-CLI mutation counted before it ran after a first attempt silently re-measured the unmutated arm; doc = docs/GOLDEN-CI.md 'The link is a third axis' (how to read the LINK line, what relay/loss/jitter each mean, and that the probe does not gate). Kin REQ-BENCH-STEP-LEDGER (the row shape these writers share, and whose parity gate now DISCOVERS its writers instead of listing them), REQ-CI-FREE-SPACE-PREFLIGHT (the same predicate's disk axis, amended the same day). [OK] REQ-CI-LOAD-BEARING-PATCH-PIN required: [impl, unit] stages: -doc +impl +unit -int A `[patch.crates-io]` pin that FIXES A SHIPPED DEFECT is asserted still in force by `xtask check`, in BOTH of the two ways it can silently lapse. (INFRA-REGISTER IR-4 part 1, releases#125 fix-lane intake; hertz 2026-08-03.) THE PIN AT RISK: swarm-discovery 0.6.0-alpha.2's `updater::gc` answers a send to a stopped actor by scheduling another gc task 10ms later — a ~100 Hz retry loop per destroyed updater that can never reach a live receiver, so every updater recreation leaks one more loop. Latent in EVERY daemon since v0.38.1; the field cost was a kitsubito daemon burning two cores for two weeks (~9 accumulated loops). Fixed upstream in rkuhn/swarm-discovery PR #27, which is in NO crates.io release (0.6.3 predates it) and iroh pins swarm-discovery to EXACTLY 0.6.0-alpha.2, so no published version satisfies the graph — the tree carries a minimal-delta backport fork pinned by `rev`, i.e. content-addressed. TWO FAILURE MODES, WHICH IS WHY ONE ASSERTION IS NOT ENOUGH: (1) a routine `cargo update` or an iroh bump re-resolves Cargo.lock back to the registry crate while the stanza still sits in Cargo.toml looking correct — the lock disagrees with the manifest, or resolves from the registry outright; (2) the STANZA IS DELETED, after which there is nothing left for the lock to disagree with and a lock-only check reads clean over a reintroduced defect. So the manifest is asked whether the pin exists AT ALL, and only then is the lock asked whether it agrees. THE EXPECTED REV IS READ FROM THE MANIFEST, never duplicated into xtask: a third copy of the sha is one more thing to forget, and a legitimate re-pin would then red on the wrong file. What IS hardcoded is the crate plus the reason its patch is load-bearing — the judgement a lock file cannot carry and a human needs in order to decide whether dropping it is safe — and the refusal PRINTS that reason plus its own drop condition (upstream ships PR #27 AND iroh's pin reaches it), so the guard states when it should be removed rather than becoming permanent by default. Removing a crate from the guard list is therefore a reviewed edit in the same commit, not a silent lapse. Gate: impl = LOAD_BEARING_PATCHES + check_load_bearing_patch_pins wired into `xtask check` (already a golden docs-drift step on both OS legs, so this rides an existing leg rather than adding one); unit = the three failure modes each proved separately (deleted stanza, registry resolution, moved rev), the two parser traps that would make the guard answer for the wrong thing (a prefix-named crate, the trailing `[[patch.unused]]` table), and an arm that runs the real workspace so the guard cannot ship green against a tree it does not actually hold. Kin REQ-CI-WINDOWS-PHASE-A-BOUND, REQ-HEAVY-UNIT-CLASSIFICATION (same `xtask check` seam: a rule that outlived the memory it depended on). [OK] REQ-CI-LOCAL-GATE-DISPOSAL required: [] stages: -doc -impl -unit -int Local gate-battery parity (path-scoped reap before target disposal + free-byte proof): a local self-hosted gate battery that creates a throwaway Cargo target reaps only process trees owned by that battery, with ownership proved by executable path or owned temporary/cache root under its workspace or declared target roots — never by process name alone; proves the scoped survivor census is zero BEFORE deleting the target; and reports drive free bytes before and after disposal. It MUST NOT machine-wide kill spt processes. Evidence: six Phase-B child daemons outlived suite completion for about two minutes in-job and indefinitely in local rigs; foreign job processes exited with the job; post+30s scoped census was zero. This extends existing CI end-reap semantics to local gate rigs, not production lifecycle behavior. [OK] REQ-CI-NOTIFY-ATTRIBUTION required: [impl, unit] stages: -doc +impl +unit -int CI notifications emit an explicit diagnostic when commit attribution is missing or unparseable rather than silently treating it as an empty successful attribution (REMOTE-FRICTION #304, IR-47). Read the exact project convention Co-authored by: from raw commit bodies; preserve that space-spelled key, which git trailer tokenization does not parse. Focused selftests cover valid attribution and missing or malformed attribution. [OK] REQ-CI-NOTIFY-CANCELLED-CLASS required: [impl, unit] stages: -doc +impl +unit -int releases#107: the CI notifier classifies CANCELLED separately from FAILURE, so a cancelled golden run stops paging the fleet as a red. `.github/ci/ci-notify.sh` `check()` (lines 94-111) folds `failure | cancelled` into one `failing` string and renders `verdict="FAILURE"` for any non-empty value. PRECEDENCE IS THE WHOLE RISK: a genuine failure normally CANCELS its siblings, so mixed failure+cancelled IS the shape of a real red — ANY `failure` present ⇒ FAILURE verdict (cancelled jobs may still be listed as context); ONLY zero failures AND ≥1 cancelled ⇒ the cancelled class, worded so it does not contain the word FAILURE. `skipped` stays non-red (REQ-CI-DOCS-ONLY-THIN deliberately skips jobs on docs-only PRs). Getting the precedence backwards silences real reds. Gate: impl — the split classes plus a LIBRARY seam, because the script is untestable as written (it `exit 0`s early when `spt` is absent from PATH and otherwise proceeds to real sends). Minted proposing a `CI_NOTIFY_DRY_RUN` env knob; SHIPPED as `SPT_CI_NOTIFY_LIB=1`, which sources the file for its `compute_verdict`/`compose_body` functions and returns before the probe/recipient/send sections — the seam `reap-census.sh` already established with `SPT_CI_REAP_LIB`, and it exposes the BODY as well as the verdict, which the wording half of this requirement needs. Executing (rather than sourcing) with the flag set exits 2 rather than no-opping to 0. unit — a `ci-notify-selftest.sh` driving the RESULT_* matrix (all-success / single-failure / all-cancelled / failure+cancelled / skipped-only / never-ran) and asserting both the verdict and the rendered body, wired into golden.yml the way `.github/ci/reap-census-selftest.sh` is. The selftest MUST carry a POSITIVE CONTROL — that a real FAILURE still pages — not merely that cancelled goes quiet. Kin REQ-CI-DOCS-ONLY-THIN, REQ-GOLDEN-CI-LANE. [OK] REQ-CI-NOTIFY-SCOPE required: [impl, unit] stages: -doc +impl +unit -int CI notification verdicts describe only the dependency results supplied to the notifier, never imply whole-run success from that subset (REMOTE-FRICTION #304, IR-40B). Preserve failure and cancellation classification while naming the scope actually observed. Focused selftests distinguish supplied-result success from a claim about unobserved jobs. [OK] REQ-CI-POSTJOB-DAEMON-REAP required: [impl] stages: -doc +impl -unit -int A CI job REAPS ITS OWN test-spawned daemons at battery end, in-job, and logs a process census at job start AND job end so contamination and reap effectiveness are visible in every run's log. (Load-flake family leg 1, doyle-ratified 2026-07-22 from deployah's third-run analysis.) THE SIGNATURE THIS CLOSES: a DIFFERENT single daemon-spawning test dying per run with a bare exit 1 and NO assertion output — process-level death, not a failed assert — while sibling tests in the same families pass alongside it, on BYTE-IDENTICAL code. Evidence: release PR #56 ran four times over a zero-.rs-delta tree; runs 1/2/3 killed brain_decouple (twice, on a disk-starved box), then adapter_translate, then adapter_digest at 105.9 GB free; box census during runs showed 43 live spt-family processes and 6486 handles against an 1881-test Phase-A full-parallel battery; run 4 went GREEN once disk and leaked session-0 daemons were cleared. WHY IN-JOB IS LOAD-BEARING AND NOT A CONVENIENCE: there are TWO leak populations on hfenduleam. Population A is session-1 (agent/gate-spawned) and is sweepable by path from any shell. Population B is SESSION-0, spawned by the actions.runner.* service — a session-1 shell CANNOT kill those (Access denied; ExecutablePath unreadable) even though they are healthy. Every CI run therefore leaves session-0 daemons behind that contend with the NEXT run while also leaking its own mid-run, and an external sweep can never reach them. The runner's own job context owns its session-0 children, so only a post-job step inside the job can reap them. KILL SCOPE IS NARROW AND PATH-VERIFIED PER-PID AT KILL TIME (never machine-wide, and never trusting the census snapshot, whose pids can be recycled): eligible only under the run's own build roots — CARGO_TARGET_DIR, the workspace target, the notify-adapter checkout's target, RUNNER_TEMP, and the pinned n1 old-broker build cache. TWO HARD EXCLUSIONS are checked AFTER the root test rather than instead of it, so that live infra survives a bug in the root computation: anything under an spt-core/bin/ install prefix, and any owl binary. A process whose image path cannot be READ is reported but NEVER killed — unreadable means unverifiable and the safe direction is to leave it standing. A BOUNDED SETTLE precedes the kill pass so a cleanly-exiting daemon is not counted as a leak; after it, a survivor is a leak by definition, which is what makes the strict-mode trigger a mechanism rather than a judgement call. Rides BOTH the test and n1-gate jobs (doyle scope ruling): n1_pairing spawns real daemon trees from the workspace and from the out-of-tree pin cache, so reaping one job leaves half the cause standing. Gate: impl — the two census/reap scripts under .github/ci/ plus their job-start and always() job-end wiring on both jobs, both runners. Kin REQ-CI-WINDOWS-PHASE-A-BOUND (the other cause-side leg), REQ-CI-DOCS-ONLY-THIN (recipe-layer precedent: impl-only, no product code). [OK] REQ-CI-PRERELINK-EXE-LOCK-REAP required: [impl] stages: -doc +impl -unit -int NO CI step that RELINKS a workspace binary runs while an earlier phase's leaked binary is still executing from the same target. (BAROMETER golden triage, hertz RCA 2026-08-02, releases#123.) THE DEFECT THIS CLOSES IS PLACEMENT, NOT MECHANISM: REQ-CI-POSTJOB-DAEMON-REAP's reap already kills exactly the right processes with exactly the right scoping, but it is the LAST step in the job, so it runs after every step it would have saved. Windows cannot overwrite a running image, so a leaked `spt.exe` from Phase A/B makes the next `cargo test -p spt --test ` die with 'failed to remove file ...\target\debug\spt.exe: Access is denied. (os error 5)'. EVIDENCE, twice out of twice on the same head: golden 30754060525 and 30758010357, both Windows `test` jobs — job-start census scoped=0 (so the leak is generated IN-job, not inherited), pre-reap census scoped=4 with the same handle-count signature both runs (two ~258-260 daemons + two ~134 brains), job-end reap killing 4 and 3 respectively AFTER the job had already died of them. The second run also shows the masking: run 1's Phase-A red hid this, and every run that survives Phase A dies at the notify relink instead. THE FIX IS CLASS-SHAPED: the guard rides EVERY relink-after-an-E2E edge (notify, installer, docs-drift, and the two-host gated-CLI step in twohost-a), not only the edge that reddened a run. ONE PREDICATE OVER ONE POPULATION: it is the same reap-census script under a `prerelink` phase, so a mid-job reap can never classify a process differently from the job-end reap; the pre-existing inline `Get-Process | Stop-Process` before the docs gate is REPLACED by it rather than left as a second, weaker rule (it scoped by GITHUB_WORKSPACE alone, missing CARGO_TARGET_DIR/RUNNER_TEMP/n1-cache roots, re-verified nothing at kill time, and printed no evidence). Census labels carry the phase (`prerelink-pre-reap`, `prerelink`) so a mid-job census is never counted as an end-of-job one by the strict-mode trigger. STRICT STAYS AN END-OF-JOB MECHANISM: a prerelink survivor warns and never reds its own step, because the relink it guards is the very next step and fails with the exact file it could not remove — a louder and more specific signal than a reap step exiting 1. WINDOWS-ONLY BY CONSTRUCTION: Linux replaces a running exe happily; role B (Linux) of the two-host pair needs no guard. Gate: impl — the `prerelink` phase in .github/ci/reap-census.ps1 plus its four wirings in golden.yml. No unit stage, for REQ-CI-POSTJOB-DAEMON-REAP's stated reason: the evidence is a recipe the runner executes and the acceptance is read off the run log (a `CI-REAP-PRERELINK summary:` line before each relink, and no os-error-5). Kin REQ-CI-POSTJOB-DAEMON-REAP (same machinery, the other placement), REQ-CI-WINDOWS-PHASE-A-BOUND (the co-tenant load leg). [OK] REQ-CI-TOOLCHAIN-VERSION-PRINT required: [impl] stages: -doc +impl -unit -int Every golden run PRINTS the toolchain that judged it, on BOTH OS legs, from inside the job. (INFRA-REGISTER IR-4 part 3, carrying IR-9; hertz 2026-08-03.) WHY A PRINT IS EVIDENCE AND A BOX IS NOT: both golden boxes own rustup-managed toolchains that drift independently, so 'did CI resolve this the way I did?' — the question the acaaa4f lock comparison needed answered — had NO answer in any run log. MEASURED on the two boxes' INTERACTIVE accounts 2026-08-03: hfenduleam (Windows leg) cargo/rustc 1.93.0, clippy 0.1.93; kitsubito (Linux leg) cargo/rustc 1.96.0, clippy 0.1.96 — three minor versions apart, which is IR-9's class with numbers on it. That measurement is a PRIOR, not the fact: a runner service account's toolchain is what judges a lane and an interactive shell on the same box does not prove it, which is precisely why the step prints from inside the job. Direction of the skew is deliberately NOT asserted — clippy renames and removes lints as well as adding them, so 'newer is stricter' is unmeasured. IR-9's docs half (align the boxes, or declare one authoritative clippy leg) is HELD by doyle's ruling of the same day: rule it once this step reports runner-account versions, rather than on the prior. STEP SHAPE: one `TOOLCHAIN ` line prefix on every line of both legs so a run's toolchain is one grep; cargo, rustc, and clippy versions are the load-bearing three and their absence reds the step; the rustup active-toolchain line is a convenience and its absence must NOT red a step whose whole job is to print — which needs a different construction per shell, and both were exercised before shipping rather than reasoned about. Windows: under GitHub's pwsh wrapper (`$ErrorActionPreference = 'stop'` + a trailing `$LASTEXITCODE` check) a MISSING COMMAND is a terminating error, so rustup's absence is TESTED with Get-Command rather than caught, and a present rustup's non-zero exit is cleared before the wrapper reads it. Linux: no `| head` anywhere — under `set -o pipefail` a producer SIGPIPEd by an early-exiting head surfaces as exit 141 and reds the print, so first-line trimming is parameter expansion. Gate: impl = the two `Toolchain versions` steps in golden.yml's `test` job (the job that builds, tests, and lints, so the one whose resolver and lint set a reader is asking about). Kin REQ-LOCK-TOUCHING-LANE-PROCEDURE (the procedure that sends an author here), REQ-CI-LOAD-BEARING-PATCH-PIN. [OK] REQ-CI-WINDOWS-PHASE-A-BOUND required: [impl, unit] stages: -doc +impl +unit -int Phase-A nextest parallelism is BOUNDED on the Windows self-hosted leg, so the full-parallel 1881-test battery stops saturating a box that also hosts the live agent fleet. (Load-flake family leg 2 = deployah option (c), doyle-ratified 2026-07-22.) Windows leg ONLY: kitsubito does not host the fleet, and its parallelism is deliberately untouched. MECHANISM AND WHY THIS ONE: the bound lives in a [profile.ci-windows] nextest profile selected by a STEP-LEVEL NEXTEST_PROFILE expression on the Phase-A step, NOT by a per-OS pair of Phase-A steps — duplicating that step would put a FOURTH copy of the filter string in the tree, and .config/nextest.toml's own header names keeping THREE in sync as the standing hazard. THE NON-INHERITANCE GATE-CHECK (doyle demanded it discharged, and it did not come back clean): a custom nextest profile inherits scalar settings from default but NOT profile.default.overrides — the argument cuts both ways. Of the five overrides on default, exactly ONE is kind(test): the by-binary integration filter, which is Phase B's alone and needs no mirror. The other FOUR are the FLAKE-LEDGER: attach_viewport_reconnects_across_a_broker_bounce class — real-broker units that are kind(bin)/kind(lib), escape the by-binary filter, and therefore RUN IN PHASE A depending on the heavy-broker-pty group to stay serialized (rc::tests four names, (applyhost|livehost|pump)::tests, wansend::tests, api::startup::tests). Shipping the profile without them would have SILENTLY UN-SERIALIZED ON WINDOWS the exact class whose un-serialization produced two separate 240s timeouts. They are mirrored verbatim, and the parity is ENFORCED rather than remembered: xtask check's check_phase_a_profile_parity fails the build when a default override that is not kind(test) is missing from ci-windows, keyed on that SHAPE and not on a name list — the same posture as that ledger entry's second half, whose whole lesson is that a class written in prose and shipped as an enumeration decays. A DRIFTED mirror reads as missing, not as present: the invariant is that it is the SAME filter in both profiles, not that something is in both. THE BOUND ITSELF IS CHOSEN, NOT DERIVED, and is recorded that way at the value: hfenduleam is 16 logical cores and the bound is 8 (half), for fleet-co-tenant headroom. No measurement separates 8 from 6 or 12 and none was run — the ratified fix is stop saturating the box, not find the saturation knee. Retuning is one integer. Gate: impl — the ci-windows profile with its mirrored overrides plus the ci.yml step-level profile selection plus the xtask parity check; unit — the parity predicate's exemption rule (a kind(test) override needs no mirror), its detection of an unmirrored Phase-A override, its rejection of a DRIFTED mirror, and a case over the REAL checked-in config so the check cannot ship green against a tree that already violates it. Kin REQ-CI-POSTJOB-DAEMON-REAP (the other cause-side leg), REQ-HEAVY-UNIT-CLASSIFICATION (the check one layer in), FLAKE-LEDGER: attach_viewport_reconnects_across_a_broker_bounce + the HEAVY-AT-BIRTH ruling (.config/nextest.toml). [OK] REQ-CLI-1 required: [impl, unit] stages: -doc +impl +unit -int spt endpoint noun namespace: absorbs fork/suspend/wake/shutdown/rename/stop/digest + access (ported 1:1: allow|revoke|open|list, decision 21) + description (ex-resources blurb; bare=show, set=author); merged endpoint list [--local|--subnet ] grouped by subnet with SELF pinned, --detail adding the ex-resources yellow-pages blurb projection; bare spt endpoint = the list (M8 decisions 1-2, 25). SUPERSEDED (F-025 item 3): the LISTING SHAPE now lives in REQ-ENDPOINT-LIST-NODE-GROUPED (node-grouped over unique instances, not grouped-by-subnet) + REQ-ENDPOINT-LIST-REST-FILTER (suspended-hidden + --show-all); the `--local` flag was dropped by REQ-ENDPOINT-LIST-MERGE-LOCAL (the list ALWAYS merges local). This REQ owns only the endpoint noun NAMESPACE + parse surface — not the render shape. [OK] REQ-CLI-2 required: [impl, unit] stages: -doc +impl +unit -int spt daemon noun: run|stop|status (hidden daemon verb becomes daemon run; agent-endpoint shutdown keeps its name under endpoint); daemon status renders the pump heartbeat (last-tick recency) so a half-dead daemon is never rendered implied-healthy (M8 decisions 5, 23) [OK] REQ-CLI-3 required: [impl, unit] stages: -doc +impl +unit -int Agent hot path stays flat across the M8 reorg: send/ring/ready/whoami/how-to unchanged; notify moves to subnet notify while notif stays top-level; breaking renames land clean with no deprecation shims (zero external CLI consumers pre-spt-claude-code) (M8 decisions 3-4, 9) [OK] REQ-CLI-4 required: [] stages: -doc +impl -unit -int User-facing CLI output is human-readable: DIRECT-USER commands (e.g. adapter update/list/use) render friendly prose instead of raw CODE:RESULT markers — "claude-spt is up to date (0.2.0)." not "ADAPTER_UPDATE_UPTODATE:claude-spt: installed 0.2.0, latest 0.2.0". Strictly bounded to the direct-user surface: the adapter-PARSED bringup tokens (SEEDED/BOUND/READY/NO_SEED on seed/listen, which adapters grep) stay machine-parseable — humanization is additive (a human line beside the marker, or a --porcelain/--quiet split), never a silent rename of a dual-contract marker. The user-facing bringup composition belongs to the adapter (perri); this REQ owns only the direct-user CLI surface. (v0.9.0) [OK] REQ-CLI-BROKEN-PIPE-TOLERANT required: [impl, unit] stages: -doc +impl +unit -int #10 (F026 micro): `spt | ` must not PANIC when stdout closes early. A closed downstream reader mid-print panics with 'failed printing to stdout: The pipe is being closed. (os error 232)' (live repro: `spt daemon status | Select -First N`). Fix: tolerate BrokenPipe process-wide — a write to a closed stdout exits 0 (SIGPIPE-equivalent: a consumer that stops reading is a normal end, not a crash), without leaking a Rust panic + backtrace to the user. [OK] REQ-CLI-HELP-MARKDOWN required: [impl, unit] stages: -doc +impl +unit -int `spt --help` (and every subcommand --help) renders the inline Markdown authored in the clap doc-comments as terminal styling, never as literal markers: `**bold**` → ANSI bold, `` `code` `` → ANSI cyan, `[text](url)` → `text`. The markers are STRIPPED either way — a raw `**` or backtick must NEVER reach the user (the operator-reported v0.12.0 defect: help text reads `**ctrl-b**` and stray backticks verbatim). Color/bold escapes are emitted ONLY when the help is going to a real terminal AND color is not suppressed (NO_COLOR unset · CLICOLOR != 0 · CLICOLOR_FORCE forces on); a pipe / redirect / CI / NO_COLOR falls back to strip-only (clean plaintext, zero escapes) so machine-readable help is byte-identical regardless of marker syntax. Pure transform over the clap-rendered help string at the single run()/bare_invocation chokepoint; preserves pre-existing ANSI (CSI sequences passed through untouched), never spans markers across a newline, leaves unmatched/empty markers literal, and does not alter the help layout. (v0.12.1) [OK] REQ-CLI-JSON required: [impl, unit, int] stages: -doc +impl +unit +int A global `--json` flag (clap global=true) honored by the READ/STATUS command set: endpoint list/whoami, daemon status, subnet status/show-code, endpoint description/role, adapter list/version, notif list, grant list, access list, shell list, how-to (endpoint digest already has it). Action commands do not honor it. A shared print_json() helper + a coverage TEST asserting every command in the set emits valid JSON (guards against the missing-shared-formatter drift). Output uses explicit per-command output DTOs with committed field names — internal structs are NOT serialized verbatim (their fields would become a public contract; JSON is a consumed wire-parity surface). (v0.16.0) [OK] REQ-CLI-MONIC-TRIGGER-SECTION required: [doc, impl, unit] stages: +doc +impl +unit -int `spt endpoint monic --help` carries a composed `Trigger kinds:` section naming every kind a trigger can watch, WHAT it watches, and WHETHER ANYTHING EVALUATES IT TODAY — the third column being the load-bearing one, because `user-input` and `agent-output` are ratified but INERT, and a section that listed them beside the live kinds without saying so would present a trigger that can never fire as one that works. That is the same honesty `monic list` already owes a stored trigger, now owed at the moment of CHOOSING one. The section is composed AT RENDER TIME from a single row table whose row carries the `TriggerKind` ITSELF, so the kind's wire spelling and its evaluated-today answer are DERIVED — from the enum and from the existing `has_live_consumer` predicate — rather than restated beside them: a kind cannot appear in the help under a spelling the `--triggers` parser would reject, nor be described as evaluated when nothing evaluates it, and a kind added to the vocabulary reaches the help by the sole act of being in the table. PLAIN TEXT, no inline Markdown, for the reason the surface section carries the identical rule: the CLI renders help through `helpfmt`, and a composer in the store crate can be read by a consumer that never passes through that renderer, so a Markdown marker reaches one audience styled and the other as literal asterisks. The words match the published docs table (kind | watches | evaluated today) rather than paraphrasing it, so an operator who read the manual and an operator who read the help learned the same vocabulary. Gate: doc — the docs-site trigger-kind table these rows speak in the same words, and the generated public CLI reference; impl — the row table beside the enum it describes, the render-time composer taking the table AS A PARAMETER, and the after-help wiring at the sited verb; unit — a fixture table carrying an INVENTED row renders that row with no edit to any display site (table-drivenness proven rather than inferred from a snapshot whose rows all agree), EVERY enum variant is present in the table (a kind added to the vocabulary cannot be silently missing from the section), a live and an inert row render their differing third column, the rendered help at the sited verb carries the composer's own text byte-for-byte rather than a second literal, and that help leaks no internal code. [OK] REQ-CLI-NODE-VERB-PRIMARY required: [doc, impl, unit] stages: +doc +impl +unit -int THE PER-MACHINE SUPERVISOR SURFACE IS SPELLED `spt node`, AND `spt daemon` REMAINS A FULL ALIAS OF IT — ONE PARSE TREE, NOT TWO. The noun operators type for their own machine was `daemon`, which names the PROCESS rather than the thing the process supervises; the surface is a node's state (daemon state, member subnets, local endpoints), so `node` is the primary spelling and `daemon` is a clap `visible_alias` on the same command. THE ALIAS IS FULL AND PER-SUBCOMMAND, NOT ROOT-ONLY: bare (the status view), run (incl. the hidden --detached auto-start-ladder flag), start, stop (incl. --force), status, access + its allow/deny/remove editing arms, refresh, and the hidden brain entry (bare and stamped) all parse identically under either spelling. A root-only equivalence proves the alias resolves and proves NOTHING about a subcommand added later under one spelling, which is why the unit asserts the parsed action as a VALUE per subcommand rather than matching variants. THE CHANGE IS CLI-SURFACE-ONLY: the spt-daemon crate, the wire, and the docs model term `spt-daemon` are untouched, and `daemon access` is mirrored as a NOUN, not duplicated as policy — one parse tree, one roster store. THE DEPRECATION NOTE IS SHARED AND ALIAS-NEUTRAL (doyle fork-D ruling): clap renders help per COMMAND, not per alias, so the note lives in the one `///` block and is worded to be TRUE under both spellings — deprecation when read on `daemon --help`, information when read on `node --help`. No argv sniffing, no stderr-only notice, no second variant. THE BRAIN SPAWN ARGV KEEPS THE `daemon` SPELLING (doyle fork-A ruling, KH-2.3): brain_child_args builds `daemon brain --generation N --start-reason R`, and the compatibility it exists for runs toward binaries that PREDATE the alias — an N-1 parser knows only the old spelling and an alias cannot rescue a parser that never had it, so migrating the spawn site is a later milestone gated on a fleet version floor, never this lane. REMOVAL OF THE ALIAS IS BLOCKED ON AN INSTALL-ARTIFACT MIGRATION (doyle fork-C ruling) AND THAT CONSTRAINT RIDES THE DOC SURFACE, not this file alone: installed OS service units and scheduled-task rungs on every deployed machine already carry `spt daemon run`, and renaming the verb does not rewrite them, so a future milestone must migrate those artifacts before scheduling removal. THE DEPRECATED SPELLING IS DOCUMENTED, NOT HIDDEN (doyle fork-E ruling): `visible_alias` rather than the hidden `--deferred`-style precedent, so the alias reaches the generated reference as a findable deprecated row — a deprecation an operator cannot find is not a deprecation. Gate: impl — the primary/alias rename with the shared note, the dispatch arm, the surface-help path table moved to the primary spelling, and the KH-2.3 spawn-argv sentence; unit — per-subcommand alias equivalence over the enumerated surface, the bare form defaulting to the status view under both spellings, and the note rendering in BOTH spellings' long help (the shape-(i) choice made visible, so moving the note to a runtime print reds here); doc — CONTEXT.md's disambiguation line separating this VERB from the retired `spt-node` separate-process deliverable, plus the reference regen carrying the deprecated row and the removal constraint. Kin REQ-CLI-2 (the noun this re-spells), REQ-HAZARD-HANDOFF-ARGV-COMPAT (the spawn argv it must not modernize), REQ-DOCS-5 (the generator whose drift gate must be green on the regen). [OK] REQ-CLI-OUTPUT-MARKDOWN required: [impl, unit] stages: -doc +impl +unit -int Human-prose COMMAND OUTPUT (not just `--help`) renders the inline Markdown authored in its source strings as terminal styling, never literal markers: `` `code` `` → ANSI cyan, `**bold**` → ANSI bold, `[text](url)` → `text`, markers STRIPPED either way. REQ-CLI-HELP-MARKDOWN only hooked the clap `--help` chokepoint, so command output still printed raw Markdown (audit: `spt how-to` topic text showed `# headers`/backticks, `spt subnet`/`subnet status` hint footers showed stray backticks, the daemon-status `not running` line, the `ENDPOINT_RUN_STARTED` attach hint, and the daemon's `SUBNET_DETACHED` startup line — 13 prose surfaces). The same line-bounded pure `helpfmt::render` is applied at each emit site, color-gated by the OUTPUT STREAM's own tty (`stdout_color` for print/println, the new `stderr_color` for eprintln). HARNESS-SAFETY (binding): color is tty-gated, so an adapter (piped / non-tty / NO_COLOR) gets STRIP mode = zero ANSI + markers removed; every dual-contract MACHINE token on a rendered line (`ENDPOINT_RUN_STARTED:`, `NO_SUCH_TOPIC:`, `SUBNET_DETACHED:`) carries NO Markdown markers, so it survives strip byte-intact — the adapter parse is never perturbed. Pure-machine output (the `` envelope, bringup parse-tokens SEEDED/BOUND/READY/NO_SEED, `--json`, QR) is NEVER routed through the renderer. The one spt-daemon source string (`SUBNET_DETACHED`, the bin-local renderer is unreachable from the daemon crate) is authored marker-free instead. (v0.12.2) [OK] REQ-CLI-STACK-HEADROOM required: [impl, unit, int] stages: -doc +impl +unit +int THE CLI MUST HAVE ROOM TO BUILD ITS OWN ARGUMENT TREE, WITH MEASURED HEADROOM. clap's derive-built `Command` tree is constructed recursively at the top of every invocation and its cost scales with the argument population; `main` ran `cli::run()` on the MAIN thread, whose stack on Windows is the PE-header default (1 MiB, not settable at runtime), while a Rust-spawned thread's is explicit. IR-33 measured the tree within roughly SIX net new arguments of exhausting it: U1's eight hidden bool args made EVERY invocation die with 'thread main has overflowed its stack', exit -1073741571 (0xC00000FD), INCLUDING `--version` — before any of our own code ran. The failure face is why this is a requirement and not a tuning note: it presents as a totally broken binary that names no stack, and through REQ-DOCS-5's generator (which enumerates the tree by PARSING the binary's own root help) it presents instead as a silently gutted docs page that the drift check then compares to itself and blesses. FIX: run the CLI on an explicitly-sized thread, so the argument population stops being a budget the next verb-surface lane has to fit under. THE WRAPPER MUST BE FAITHFUL, WHICH IS THE WHOLE RISK OF THE CHANGE: a panic inside the CLI must still leave the process with the SAME face it had when the panic unwound out of main — the same message through the same hook, and the same nonzero exit code (101) — and an ordinary return must exit with the INNER run's code, not the join's. A spawn failure falls back to running inline and SAYS SO, because a resource blip must not turn into a dead CLI, and a silent fallback would restore the old ceiling invisibly. Gate: impl — the sized-thread wrapper in main.rs with its fidelity mapping and its loud fallback; unit — the wrapper driven with a returning closure and a PANICKING one, asserting the code passes through and the panic maps to 101 (the prod call site passes `cli::run` to the same function, so this is the real wrapper and not a parallel spelling); int — a smoke that the DEBUG-built binary answers `--help` with non-empty stdout at exit 0, which is the only assertion that would have caught IR-33 in its own build profile and also backstops the empty-emit generator above. [OK] REQ-CLI-SURFACE-SECTION-SITED required: [doc, impl, unit] stages: +doc +impl +unit -int The composed `Control surfaces:` section rides the --help of exactly the verbs whose invocation NAMES A SURFACE, and of no others, so the vocabulary meets the operator at the moment of choosing rather than in a manual: `knock` bare, `knock send`, `knock approve`, `knock new-code`, `knock list`; `endpoint access allow` / `deny` / `remove` and the FLAGLESS `endpoint access` listing (which is an EndpointCmd variant, not an AccessCmd one — a distinct site that a subcommand-only sweep silently misses); `daemon access` and its `allow` / `deny` / `remove`; and `subnet create`. DELIBERATELY EXCLUDED and recorded as decided, not overlooked: `knock redeem` and `knock deny` (the surfaces are already fixed by the code or the knock — printing the table there invites the reader to believe they have a choice), and `endpoint fork`, which keeps its existing INLINE FORK+DISCOVER prose because a two-surface verb is better served by a sentence than by a table. The section is appended through clap's after-help placeholder, which the tree uses nowhere today — a NEW PATTERN, so the root help template's existing but unused placeholder is what carries it, and the section text itself comes from the surface table's composer rather than from any per-verb literal. Public help stays free of internal codes at all depths, including the three-deep sites the docs-site drift gate cannot reach. Gate: doc — the public CLI reference; impl — the after-help wiring at each named site, fed by the single composer; unit — every named verb's rendered help carries the section and every excluded verb's does not, the flagless listing site is covered distinctly from its subcommands, the rendered text matches what the composer produced for the same table, and A WALK OVER EVERY NAMED SITE'S RENDERED HELP ASSERTS NO INTERNAL CODE (REQ-, ADR-, F-) APPEARS AT ANY DEPTH — the row that closes the three-deep gap the docs-site drift gate cannot reach, and which earns its green two ways rather than by default: the walk's site set is PINNED (it asserts it visited exactly the named sites, so an empty or truncated enumeration fails loudly rather than passing vacuously) and the same predicate is proven ABLE TO FAIL against a fixture help text carrying a code. [OK] REQ-CLI-WIN-VT-ENABLE required: [impl, unit] stages: -doc +impl +unit -int A7 (F028, operator, Win10 conhost): ANSI emitted without VT enable → garbled console. Evidence (raw PowerShell 7, Win10 conhost): literal `←[36m` in `endpoint list` + `--help`. ROOT: ENABLE_VIRTUAL_TERMINAL_PROCESSING is enabled ONLY on the rc attach path (rc.rs:746, REQ-RC-WIN-VT-OUTPUT) — plain CLI stdout never enables it, and the color decision doesn't fall back when the console can't render VT. FIX: lift the rc.rs VT-enable into a SHARED startup helper for every colored-output path; if SetConsoleMode fails (or stdout isn't a console), STRIP colors (the ansi_wrap/helpfmt color=false path already exists — plumb the decision, not new rendering). Windows Terminal masks this (VT always on) — TEST on raw conhost. See triage A7. [OK] REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS required: [int] stages: -doc -impl -unit +int MSG-IDENTITY W6 / F-039 leg e (doyle W6 LOCK 2026-07-10, minted per amendment 3 — hertz's five invariants VERBATIM from his RCA fix-shape item 5): 'Build a deterministic black-holed-controller harness against current v0.30.6 semantics and assert: unrelated sessions continue; the bad physical connection is canceled/closed within the bound; its writer exits; a fresh viewer can attach; no lock or task remains owned by the retired connection.' The harness is the standing conformance rig for the r4 SHAREDSEND fix-class — hertz's RCA discipline: only after a timestamped incident maps to a FAILING lifecycle invariant does an ownership/cancellation defect get fixed (the likely shape being complete physical-connection cancellation and writer-task join/retirement, never a broader timeout increase). Consumes REQ-CONN-POISON-ATTRIBUTION's records (conn id + lifecycle events are what make the five assertions checkable deterministically). Kin REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (the invariant class under test — its brain_decouple int stays the Windows-mandatory gate leg), REQ-CONN-POISON-DIAL-SCOPE. [OK] REQ-CONN-HEALTHY-LIFECYCLE-BOUNDED required: [doc, impl, unit] stages: +doc +impl +unit -int Healthy broker IPC starts and closes aggregate once per monotonic interval across connections, independently of request volume; poison, failure and role-transition evidence remains per-connection with original first-write time. Livehost session polling reuses its healthy carrier and names why it opens a new one; role=brain alone never identifies the supervised coordinator. Network family diagnostics remain once per real endpoint bind attempt, not IPC churn. Narrow healthy-start/close supersession of REQ-CONN-POISON-ATTRIBUTION leg d (releases#286). [OK] REQ-CONN-POISON-ATTRIBUTION required: [impl, unit] stages: -doc +impl +unit -int MSG-IDENTITY W6 / F-039 legs b-d (doyle W6 LOCK 2026-07-10, minted per amendment 3): every broker-conn lifecycle record is ATTRIBUTABLE — the W6 RCA's terminal undecidability (per-line 1:1 CONN_WRITE_POISONED churn = fresh-carrier churn OR stderr interleave artifact) exists because records carry no stable conn identity, no role/endpoint/session context, and no timestamps, and the once-per-conn poison latch hides multiplicity. THREE LEGS. (b) IDENTITY: mint a stable per-physical-conn id (monotonic u64 at conn construction — Arc::ptr_eq is the only identity today and it does not survive a log line) plus subscriber role and endpoint/session where known, stamped on CONN_WRITE_POISONED, CONN_WRITE_RETIRED, logical stall-evict, attach/resume/detach, and write-retirement records (RCA attach sites: presence nethost.rs:379, stream nethost.rs:258, controller broker.rs:891, viewer broker.rs:1073). (c) TIME: daemon stderr correlation records carry wall-clock AND monotonic timestamps (stderrlog has neither; broker+brain share one file — interleave is unresolvable without them). (d) LIFECYCLE (doyle-confirmed UNCONDITIONAL, not debug-gated): one BOUNDED set of per-conn lifecycle events — write start/timeout-cancel/transport close/writer exit/replacement-reattach (hertz RCA fix-shape items 1-3). Constraint (doyle LOCK): the split/attribution must not REDUCE total information, only correct its attribution; NO timeout-value changes; NO suppression-as-fix. Gate: unit — lifecycle records carry conn id + role + timestamps; the id is unique per physical conn and stable across that conn's records. Kin REQ-CONN-POISON-DIAL-SCOPE (leg a, the token split these fields ride on), REQ-CONN-BLACKHOLE-LIFECYCLE-HARNESS (leg e, consumes these records), REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (behavior invariant preserved). [OK] REQ-CONN-POISON-DIAL-SCOPE required: [impl, unit] stages: -doc +impl +unit -int MSG-IDENTITY W6 / F-039 (deployah field-acceptance follow-up 2026-07-10, RCA-FIRST — mint per the v0.30.6 PASS handoff): ambient CONN_WRITE_POISONED log-churn correlates 1:1 with PUMP_PEER_FAIL submit-dials to OFFLINE peers (enlyzeam/kitsubito/gravity) with NO wedge and NO freeze — pre-existed the blackhole rig = log-noise/mislabel, not a defect in the r4 fix. CODE CONTEXT: conn.rs poison_and_cancel emits the loud CONN_WRITE_POISONED line for a write that 'exceeded its bound (OR FAILED)' (conn.rs:181) — the fast-FAIL branch (broken pipe / conn refused on an already-dead counterpart) shares the log tag with the TIMEOUT branch that is the field-acceptance wedge observable, so routine conn teardown under offline-peer dial churn reads like poison events. RCA-FIRST: pin the exact write site that fails per PUMP_PEER_FAIL cycle (BrokerConn is broker-side — which broker conn write rides each pump dial failure? status/event fan-out to a departed subscriber? brain-side notification?) BEFORE changing anything — the correlation mechanism is unpinned. FIX SHAPE (post-RCA, doyle rules at lock): reserve the loud CONN_WRITE_POISONED token for the DEADLINE-EXCEEDED class (the wedge observable blackhole-controller.ps1 watches); a plain write-FAIL on an already-dead conn retires quietly (debug-level or a distinct low-noise token). MUST NOT weaken the r4 invariant: every failure path still poisons + retires the conn (REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK behavior unchanged — only the log LOUDNESS is scoped). Gate: unit — deadline-exceeded emits the loud token, fast-fail does not (both still poison); field — offline-peer churn no longer floods daemon.stderr.log with CONN_WRITE_POISONED. Kin REQ-HAZARD-SHAREDSEND-NO-BLOCKING-WRITE-UNDER-LOCK (the invariant this must preserve), REQ-PUMP-PEER-ISOLATION (the offline-peer dial neighborhood), [[v0306-published]]. [OK] REQ-CONSENT-1 required: [impl, unit] stages: -doc +impl +unit -int Consent grant store: capability x subject-agent x target-node rows, enforced at the target node, subnet-settable (replicates as security material near the trust store), revocable; gated-capability ids (remote-exec, instantiate-anywhere) reserved-but-refusing; v1 consumers are the shell spawn gates (CONTEXT Consent & security gates) [OK] REQ-CONSENT-2 required: [impl, unit] stages: -doc +impl +unit -int Interactive consent escalation: an ungated high-risk action routes a consent prompt to the user's most-recently-active session; allow-once / allow-always (writes a grant) / deny; pre-consent flags (can_shutdown, shell_wake_spawn_anywhere) author grants via manifest/settings (CONTEXT Consent & security gates) [OK] REQ-CONSENT-3 required: [doc, impl, unit, int] stages: +doc +impl +unit +int Per-capability approval gates (class-keyed): the require_approval enum may ride INDIVIDUAL [shell.capabilities] entries — gating the dangerous ACT, not just the spawn — with an optional class_key scoping the grant qualifier finer than the capability id ((owner endpoint x device class x node); a remembered HID-class attach grant never authorizes a storage-class attach). Reuses the grant store + interactive escalation + tighten-only floor (REQ-CONSENT-1/2 plumbing). Spawn gates govern EXISTENCE; capability gates govern ACTS — an explicitly distinct invariant (CONTEXT:283, ratified 2026-06-11 Gateway grill). [OK] REQ-CONTROL-STAMP-CONVERGENCE required: [doc, impl, unit, int] stages: +doc +impl +unit +int REGISTRY-LIFECYCLE W2 (ADR-0041 decision 4, emphasys C3 P1): control-stamp writes are session/generation-VALIDATED (or per-endpoint stamp transitions serialized) so a pre-reap KIND_SESSIONS snapshot can never relatch controlled=true after the exit-waiter reap; no fs I/O under global locks (KH 7.12); every no-session path clears all three control fields (controlled/driven_by/viewer_count). Gate: impl — validated/serialized stamp transitions; unit — stale-snapshot write refused after a newer no-session truth, no-session paths clear all three; int — poll-vs-reap interleave converges to cleared stamps (the relatch regression); doc — ADR-0041. [OK] REQ-CONTROLLER-LEASE-IDENTITY required: [doc, impl, unit, int] stages: +doc +impl +unit +int RC-RENDER-TRUTH W2 (ADR-0044 decisions 1+2, hertz same-machine --take split-brain RCA P0-A/B, doyle seam-verified broker.rs resolve_subscribe 1296-1318 + 1334-1347): each rc invocation/attach stream mints a UNIQUE controller lease id carried through SubscribeReq, the controller slot, and Input/Resize; node identity stays separate as attribution/access policy only. The ONLY silent successor/replay case is same-lease + equal-or-newer generation (ADR-0038 fix-6 dispatcher-recovery contract preserved exactly); same node + different lease = DISTINCT controller. Explicit Take on a distinct incumbent lease ALWAYS revokes loudly AND authoritatively — atomically revoke/fence the old lease and FORCE its attach stream closed, then install the taker, regardless of whether by-node strings match; the Displaced notice is best-effort (today old.tx.try_send at broker.rs:1342-1345 can DROP the notice on a Full queue while become_controller still replaces — the revoke/close must land even when notice enqueue fails; the closed stream is itself the terminal signal rc's PumpEnd::Displaced/EOF path handles). Today same_identity keys on controller_by()==by alone, intent never consulted; two same-machine windows collapse to one identity and --take silently swaps the sink leaving the incumbent input-capable but blind. Acceptable identity seam: (by, nonzero attach generation) as the logical lease. GENERATION LADDER (doyle ruling 2026-07-18, resolves fix-6/Busy conflict at T6): within the SAME by — equal gen = silent successor (dispatcher recovery, unchanged); STRICTLY NEWER gen (Control or Take) = loud+fenced supersession (full W2 revoke — without it a live replacement viewport bounces Busy off a stale-replay worker serving a zombie stream forever = post-restart lockout, fix-6 order-independence regression); OLDER gen = refused Busy (unchanged). Across DIFFERENT by: Control = Busy, Take = loud revoke. Deliberate UX: a second same-node window's plain rc loudly displaces the first (pre-W2 did the same replacement SILENTLY with the loser blind-but-interactive; loud+fenced strictly better; --view = coexistence). redispatch_stall T6 must stay green UNMODIFIED. N-1: lease field additive; absent lease (older client) degrades to legacy node-identity semantics for that subscriber. Gate: impl — lease mint + carry + loud distinct-lease Take; unit — same-lease equal/newer gen stays silent, same-node different-lease Take displaces loudly, older-gen replay still refused busy; int — hertz regression steps 1-4 + 7-8 (A gets Displaced{by} + terminal stream completion, output reaches B only, metadata identifies B, one controller slot; separate equal-lease replay test proves dispatcher recovery stays silent); doc — ADR-0044. [OK] REQ-CONTROLLER-LIVENESS-REAP required: [impl, unit, int] stages: -doc +impl +unit +int B-2 (REMOTE-TRUTH triage §B-2, REDUCED @bdc1242): a stale ONLINE+CONTROLLED stamp on a live-session perch self-heals — the info.json driven_by/controlled RECORD is made to match the broker's SINK-TABLE TRUTH. ROOT (persisted-stale-stamp class, doyle Q1): the livehost control reap gates on !has_session (reconcile_hosted_liveness), and a brain-only update KEEPS the session (REQ-UPD-3), so a controller stamp that went stale WHILE the session lived was never re-derived from broker truth. NOT a transport bug: (a) a persisted conn is the REQ-UPD-3 feature; (b) an idle-severed conn eventually EOFs via QUIC keepalive → handle_conn detach (path 1) — and the reason paths 1-3 previously failed to clean up was the B-1 broker floor-lock POISON WEDGE (cleanup panicked under the poisoned lock), now fixed. REDUCTION (doyle, my ground-truth): the prescription was 80% pre-built — converge_perch_stamps (broker.rs, REQ-HAZARD-CONTROL-STAMP-CONVERGENCE) ALREADY converges info.json driven_by/controlled to the broker's controller_by/has_controller on EVERY KIND_SESSIONS poll, and the livehost reconcile already TRIGGERS that poll per tick (query_live_session_endpoints). So NO new IPC query, NO new livehost arm, NO 5th detach path — the ONLY gap is that a controller whose WRITER THREAD died (severed conn: the writer failed a socket write, or a detach dropped by the prior B-1 wedge) still reports controller_by=Some/has_controller=true, so converge keeps the stale stamp. FIX: a broker-side lazy-reap in the KIND_SESSIONS snapshot closure — OutputLog::reap_dead_controller() drops a controller whose _writer.is_finished() BEFORE controller_by/has_controller are read, so the reply + the off-lock converge both see the honest (cleared) state and the stamp clears. LOCK-SAFE: the reap drops the sink in-memory ONLY (no stamp_driven_by → no info.json I/O under the log lock, the KH 7.12/5.16 lock-across-effect discipline); the OFF-lock converge_perch_stamps writes the honest stamp. KH 7.15 held by construction: the reap only ever CLEARS, never latches driven_by; a LIVE (idle, parked-on-rx.recv) controller is is_finished()==false so it is NEVER false-reaped. RESIDUAL (doyle Q2 accepted): a TRULY IDLE severed controller (writer parked on recv, no output, conn not yet EOF'd) stays is_finished()==false and converges only on output-resume / conn-EOF — that harder active-probe case is the RESERVED REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT (SessionInfo.controller_by doc), deliberately NOT built here so the reserved seed keeps its scope. Red-first: a dead-writer sink → reap clears it (controller_by honest None → converge clears the stamp); a live-writer sink → UNTOUCHED (no-false-reap control). [OK] REQ-CONV-1 required: [impl, unit] stages: -doc +impl +unit -int Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20) [OK] REQ-CONV-2 required: [impl, unit] stages: -doc +impl +unit -int Event-driven advertisement: endpoint online/offline transitions (ready-listener start/stop, rest-state transition, perch death) trigger an immediate advertise_local + peer push as a WAKE of the existing pump loop (no second advertisement path — epoch lease + visibility gates ride unchanged); the cadence stays the steady-state floor (M8 decision 15) [OK] REQ-CRC-SWAP-OLD-DISPLACE required: [impl, unit] stages: -doc +impl +unit -int C1 (F028, infra; memory crc-swap-old-litter-brick, RCA'd ENLYZEAM + confirmed systemic): crc_swap `.old`-litter bricks every subsequent adapter update. apply_crc_swap Phase-3 `let _ = remove_file(.old)` (spt-daemon/src/crc_swap.rs:129-133) silently fails whenever ANY pre-update process still image-maps the old binary (NORMAL on a live box — endpoint launch children survive updates). The NEXT update's first commit-op rename(exe->exe.old) = MoveFileExW(REPLACE_EXISTING) must delete the mapped .old -> win32 err 5 -> whole apply fails + rolls back FOREVER, context-free. FIX: (i) DISPLACE not replace — when .old exists, rename it aside to a unique suffix (rename succeeds on mapped files; spt's own updater already does spt.exe.old-); GC stale .old.* opportunistically. (ii) Wrap swap io errors with op + path (`rename claude-spt.exe -> claude-spt.exe.old: …`). Unit seam exists (crc_swap tests). See triage C1. [OK] REQ-CREATE-BIND-REST-ACTIVE required: [impl, unit, int] stages: -doc +impl +unit +int A successful fresh `endpoint run --create` bind MUST normalize rest intent to active IN THE SAME lifecycle transition — it must not leave `rest_state=suspended` from the prior life, and must never require a separate explicit `endpoint wake` to make a freshly-bound-online endpoint's rest vocabulary truthful. RE-PROVEN PRESENT on 0.44.0 (hertz W0 item 3, 2026-07-27, isolated build): `endpoint stop` -> raw status=offline rest_state=suspended session_id=5c93...; successful `run --create --start` minted session 714260b31d440df3 status=online — rest_state STILL suspended; independent repeat matched (new session 595f..., same sticky suspended). Sibling probes: fresh create with no prior perch carries NO rest_state field (the field itself is inherited residue, not a fresh stamp); explicit `endpoint wake` on the reproduced row flips suspended->active (the transition exists and works — it is simply never taken by the create/bind path). Downstream symptom from the original F-034 filing: `endpoint suspend` NO_EDGEs on a visibly ONLINE endpoint because the recorded rest state already claims suspended. FIX SHAPE: the create/bind success path stamps rest intent active atomically with the online transition (terminal-normalize family — same transition, never a follow-up repair step); deliberate suspend-while-online semantics (ADR'd rest vocabulary) must be preserved — the normalization applies to FRESH binds inheriting a dead life's rest state, not to a live session an operator suspended. Kin: KH 7.49 (a verb never stamps a state it did not cause — this is the inverse: a transition failing to stamp the state it DID cause), terminal-normalize work 0.41-0.43 (which closed the adjacent shapes but never reached this seam). [OK] REQ-DAEMON-1 required: [impl, unit, int] stages: -doc +impl +unit +int One per-machine spt-daemon owning all per-machine state [OK] REQ-DAEMON-2 required: [impl, unit, int] stages: +doc +impl +unit +int Broker/brain split for seamless self-update [OK] REQ-DAEMON-3 required: [impl, unit, int] stages: -doc +impl +unit +int Any api invocation auto-starts the daemon if absent [OK] REQ-DAEMON-4 required: [impl, unit, int] stages: -doc +impl +unit +int Honor every KNOWN-HAZARDS invariant [OK] REQ-DAEMON-5 required: [impl, unit] stages: -doc +impl +unit -int Pump liveness: the peer pump writes a last-tick heartbeat consumed by daemon status / subnet status (decision 23 render legs in REQ-CLI-2/REQ-SUBNET-8); the daemon supervises the pump task — a panic is caught, logged loudly, and the pump restarts with capped backoff (≤5 min), so a 5.9-class death self-heals visibly instead of silently halving the daemon (M8 decision 23; field motivation: hfenduleam 2026-06-07 half-death) [OK] REQ-DAEMON-6 required: [impl, unit] stages: -doc +impl +unit -int Service-aware `daemon start`/`stop`: when an OS service manager has a registered spt-daemon for this user, `spt daemon start` and `spt daemon stop` drive THAT service (so stop doesn't IPC-kill a unit that auto-restart-fights for the broker socket — the kitsubito 2026-06-08 loop). `start` graduates from a `run` alias to a first-class background verb (ensure-up, idempotent, non-blocking); stop routes managed→manager, manual→IPC. Linux=systemd user unit (`systemctl --user start|stop|is-active spt-daemon`, detected by unit-file presence); Windows=no controllable manager (the logon task is boot-only), so start=detached spawn / stop=IPC. [OK] REQ-DAEMON-7 required: [impl, unit] stages: -doc +impl +unit -int `daemon run` is foreground-consistent on every platform: the invoking process IS the daemon, blocks until signalled, never auto-detaches or respawns into an invisible background task. The detached/de-elevated background behavior lives ONLY in `start`. Windows: an ELEVATED `daemon run` refuses with guidance (use `start`, or an unelevated shell) instead of respawning detached/de-elevated and vanishing (KH 5.7 preserved — it still never serves elevated). [OK] REQ-DAEMON-8 required: [impl, unit] stages: -doc +impl +unit -int Internal auto-start prefers the service: `ensure_running` (any spt command's implicit daemon start, REQ-DAEMON-3) routes through the service-aware start path — when a manager has a registered service it starts THAT, never a competing manual `spawn_detached` daemon that would fight the service for the socket. [OK] REQ-DAEMON-9 required: [impl, unit] stages: -doc +impl +unit -int Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders. [OK] REQ-DAEMON-BITS-AMBIGUITY required: [] stages: -doc -impl -unit -int SEED (inactive, RCA-first — do NOT close on agreement): nothing on a node surfaces WHICH BITS ARE SERVING, and the same silent wrong-state shape bit twice in one day (2026-07-25). Case 1 (adapter-side echo): a post-reboot ensure race left a dev-build alchemy Hub Daemon serving release shells — version skew visible only by manually comparing process image paths. Case 2 (core, field-measured by flynn): TWO spt daemons resident with live brains on one node — the installed main daemon (owning ALL sockets: the 5474 listeners and every established connection, single home_tag pipe family) and an orphaned scratchpad-built daemon (auto-started into the node by ensure_running from a stray dev-binary invocation at 16:18, holding zero sockets, resident for hours) — while `spt --version` on any binary file answers nothing about which process is answering. Measured sharp edges to carry into the RCA: (a) exe path and resolved HOME are independent — the orphan ran scratchpad bits against the DEFAULT home, so 'where the binary lives' predicts nothing about 'whose state it mutates'; (b) the brain.ready breadcrumb is ONE FILE PER HOME, LAST-WRITER-WINS, keyed by generation — with two brains in one home the stamp can be written by the daemon you are NOT gating on, so any readiness/identity gate that trusts it must first establish single-writer; (c) the breadcrumb's exe_hash (SHA-256 of resident bytes captured at process start) is the RIGHT discriminator — image path answers what is on disk, not which bits are answering — but only under (b)'s single-writer precondition; (d) reap order matters: killing the breadcrumb's last writer leaves the file describing a dead brain's bits until the survivor's next ready write, so any bits-gate readback must be re-established AFTER a reap, never carried across one. Open RCA questions before any fix is designed: why did the second daemon's cold-start not refuse against the live singleton (socket-bind loss is survivable-and-resident today — is that the right posture?); what should ensure_running check BEYOND socket liveness (bits identity?); where does 'which bits are serving' surface to an operator (endpoint list? daemon status verb?). Kin: the NEVER-SEALING-OBSERVABILITY candidate (same shape — silent wrong-state only a human staring at the right field catches). Proposed by todlando (his lane), relayed by flynn with the socket-ownership + exe_hash measurements; seeded by doyle. The orphan pair was reaped by path 2026-07-25 (verified by exact ExecutablePath, supervisor before brain); the reap resolved the instance, not the class. RCA POINTS FROM THE PROPOSING LANE (todlando, extended into THIS record 2026-07-26 rather than minted as a second seed): (1) ORDERING — 'do both pids resolve the same spt_home?' is the FIRST question, not a co-equal fact, because every other discriminator is conditioned on its answer: brain.ready is `/brain.ready`, ONE path, single-writer BY DESIGN, so a shared home makes the breadcrumb a contended file and `generation` — the readiness gate's key, which exists precisely to prevent false promotion — becomes satisfiable by the stamp of the daemon you are NOT gating on. Prior art on this node: default-home `home_tag` sockets already cross-talk the live daemon's hubs, so shared-home cross-talk is an established class here, not a hypothetical. (2) GATE ON RESIDENT BYTES, NEVER ON IMAGE PATH — AND TREAT ABSENCE AS UNPROVEN. Path answers 'what is on disk where I asked'; with two daemons on different bits the only question that matters is 'which bits answered me'. Path is the exact field that has already lied in the field: KH 6.11 — the broker resolves `current_exe()` PER SPAWN (`crates/spt-daemon/src/brainproc.rs`), which on Linux is inode-tracking, so an `update apply` rename made the respawn land on the OLD bytes while readiness passed and the trial recorded `applied:N` (kitsubito v0.4.1); `exe_hash` (lowercase-hex SHA-256 of resident bytes, captured ONCE at process start — `current_exe_hash`, `crates/spt-daemon/src/brainproc.rs:402`) exists BECAUSE the path-derived belief was provably wrong, and the enlyzeam 0.3.0-under-0.3.2-on-disk case is the same record/reality divergence one layer up. Constraint any observability gate must inherit and must NOT weaken: `exe_hash` is ADDITIVE/BEST-EFFORT — omitted when the self-read fails, `None` on any pre-D7 stamp — and today's `bytes_gate` deliberately degrades an absent hash to readiness-only promotion with a loud `PROMOTE_BYTES_UNVERIFIED` (N-1 compat, `brainproc.rs:1042`). An operator-facing 'which bits are serving' answer must degrade the OTHER WAY: absent hash reads UNPROVEN — never PASS, and never a silent fall back to image path. A compat degrade that is correct for ACCEPTING an update is wrong for ASSERTING an identity. (3) SERVICE OWNERSHIP IS DECIDED BY SOCKET/HUB BINDING, NOT BY START TIME. Case 2 measured the instance (main holds all 5474 listeners + every established connection; the scratchpad daemon holds zero sockets and was the LATER start) but the INFERENCE RULE is what must survive the instance: a later start is not thereby the loser and an earlier start is not thereby the server — 'who is serving' is answered by who owns the binding, so any gate, reap, or diagnostic that ranks candidates by pid or start time is guessing at the one fact it is supposed to establish. (4) REAP-ORDER SYMMETRY — (d)'s re-establish-the-gate-AFTER-the-reap rule holds identically on the REVERT path; a bits-gate readback may never be carried across a reap or a rollback in either direction. (5) SINGLE RECORD — this seed is the one home for the class (premature-closure guard: convergent reads are not a root cause, and a green re-read after a reap assigns owner without closing); the socket-bind-loss posture question stays OPEN inside it, and the orphan-pair instance stays closed. [OK] REQ-DAEMON-REFRESH required: [doc, impl, unit, int] stages: +doc +impl +unit +int THE-FORKENING W4 (operator add 2026-07-14): `spt daemon refresh` — restart the daemon BRAIN without a binary swap and WITHOUT touching the broker: exactly the apply_staged brain-cycle path (brain stop -> respawn -> readiness trial -> promote, incl. the trial-drain drive REQ-UPDATE-TRIAL-DRAIN-DRIVE and viewer-only resume REQ-BRAIN-RESUME-NO-CONTROL-STEAL) minus the swap. Recovery verb for wedged brain-held state (field motivator 2026-07-14: endpoint bringup broken on a live daemon + deployah down — today's only remedy is a full daemon bounce that kills every PTY). Broker + PTYs survive by construction (handoff invariant). Failure = the existing trial rollback semantics (old brain resumes; refresh reports loud). Gate: unit — verb routes the brain-cycle without staging/swap preconditions; int — refresh on a live daemon with a hosted PTY: brain generation changes, PTY survives, endpoint stays attached; doc — daemon docs name refresh next to stop/start. Kin apply_staged (the path it reuses), REQ-UPDATE-TRIAL-DRAIN-DRIVE, REQ-BRAIN-RESUME-NO-CONTROL-STEAL. [OK] REQ-DAEMON-RESTART-SELF-INTERRUPTION required: [] stages: -doc -impl -unit -int SEED (inactive — operator-workflow trap, field-paid 2026-07-25): an spt-hosted context CANNOT restart the daemon, because the restart's STOP phase tears down the very context running the restart command — the START half never executes and the daemon stays down until an operator starts it by hand. Field case (this node, 17:47–17:50): an agent-kicked restart's stop-begin swept the initiating context and everything sharing its Job Object (flynn's shell + the relay-hopped alchemy Hub, cross-context job members — see REQ-SHELL-CLI-SPAWN-JOB-EXPOSURE's field-firing record); the broker drained ~2m10s to final exit (stderr ends abruptly at 17:50:05.8 amid routine traffic, no shutdown markers); the START was already dead; the operator manually started whatever binary was installed (v0.42.0), with no channel conveying which bits the restarting agent had intended — binary choice silently transferred to whoever brings the daemon up (kin: REQ-DAEMON-BITS-AMBIGUITY, the same which-bits-serve blindness). CONSEQUENCES: (a) every RESTART-REQUIRED apply (broker update, seed-skew restart per hazard 7.9) is operator-only today, and nothing documents that; (b) stop-begin kills bound shells with `close_shell` unrun, so even the GRACEFUL stop path manufactures stale-online shell records on every restart — healed at the read gates by REQ-HAZARD-SHELL-STALE-ONLINE's derivation, but manufactured nonetheless, and on pre-derivation binaries the records lie until the next bind; (c) a restart initiated from inside the system it restarts is a self-interruption with no error surface — the initiator dies believing the restart is in flight. Option space at activation: (1) daemon-owned restart verb — the OUTGOING broker spawns its successor job-neutrally (the 7.10 WMI→schtasks→breakaway ladder already exists) before exiting, so the caller only requests and its death is irrelevant; (2) a detached job-neutral restart helper (same detached_no_inherit + ladder) that survives the caller; (3) status quo + the public contract documents restart as operator-only. Whichever wins must also answer where the INTENDED-BITS declaration lives so a manual or automated starter picks the right binary (the (c)/BITS-AMBIGUITY joint). [OK] REQ-DAEMON-SERVICE-INSTALL required: [] stages: -doc -impl -unit -int F-038 RIDER (flynn nice-to-have, QUEUED not activated): a documented OS-service registration recipe or `spt daemon install-service` verb so the daemon itself survives box reboot (flynn's box runs managed_by:null = daemon-at-boot unprovisioned; kitsubito's hand-rolled systemd --user unit = prior art). NOT in MSG-IDENTITY scope — REQ-ENDPOINT-AUTOSTART covers the daemon-start-to-endpoint leg; the boot-to-daemon leg stays interim (logon scheduled task / systemd unit). Activate at an infra-provisioning milestone; shape (recipe doc vs verb) ruled then. Kin [[daemon-service-detection-gotcha]] (global-OS-state detection blind on dev box — a verb must not regress that), [[kitsubito-linux-rig]]. [OK] REQ-DAEMON-STATUS-JSON-TRUTH required: [] stages: -doc -impl -unit -int D2 (F028, perri F-d): `daemon status --json` truth drift. managed_by/managed_active read null in JSON while the HUMAN view says 'managed-by: manual — at-logon task registered' (the two surfaces disagree); and pump staleness (the STALLED diagnosis, B5) is NOT computable from JSON — only a raw pump_heartbeat_ms is emitted, no derived staleness/stalled field. FIX: JSON managed_by/active match the human render, and add a derived pump-staleness/stalled field so B5's condition is machine-observable. See triage D2. [OK] REQ-DAEMON-STDERR-PERSIST required: [impl, unit] stages: -doc +impl +unit +int W3 (LIFECYCLE-TRUTH, observability): broker + brain stderr tee to a rotating size-capped file under SPT_HOME (e.g. 2x5MB), stamped per generation. ROOT: detached daemon nulls stdio -> the 2026-07-06/07 incident window left ZERO logs (both RCAs ran blind; rigs had to recreate everything). KNOWN-HAZARDS note: never inherit handles (REQ-HAZARD-DETACHED-DAEMON-STDIO) — open the file in-process, don't pipe. [OK] REQ-DAEMON-STOP-LIVE-SESSION-WARN required: [impl, unit] stages: -doc +impl +unit +int W3 (LIFECYCLE-TRUTH, promoted old follow-wave seed): `daemon stop` with live hosted sessions warns + requires --force (or names the sessions it will kill) instead of silently killing them. [OK] REQ-DIAL-FAILED-ADDR-MEMORY required: [impl, unit] stages: -doc +impl +unit -int WAN-TRUTH #40 F2/F3 mechanism: the node remembers the exact ADDRESSES its dials just failed on — never the peer, never the chain leg (doyle ruling 2026-07-31, amending the RCA's per-leg/per-peer wording, which would have deleted the strand-heal guarantee REQ-PEER-ROUTE-CHAIN was built to provide). The record is keyed on the full address actually dialed, compared by exact equality; it is TTL-bounded and cleared by a successful dial or validation of that address, so a transient failure can never permanently ban an address that works. It is RAM-only and node-LOCAL: a restart forfeits it (one wasted dial, re-marked on the next failure), no new persisted store is introduced, and the fact never rides the gossip/roster DTO in any form. It is BUILT at the dial site (which owns the address that was dialed) and consumed by its two leaves — REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP and REQ-RECONCILE-FAILED-ADDR-REFUSE — through ONE shared helper, so neither consumer re-derives the equality. Gate: impl — the type, its construction at the dial site, the clear-on-success, the TTL, and the carry to the pump-side consumer over the existing local presence field; unit — an exact address matches and a differing address does not, a successful dial clears the record, an expired record stops matching, and the type carries no serialization onto any gossip DTO. [OK] REQ-DIAL-ROUTE-LEG-ATTRIBUTION required: [impl, unit, int] stages: -doc +impl +unit +int WAN-TRUTH #41 leg 1: every submitted dial says WHAT address it dialed and WHICH leg of the route chain produced it, at submit and again on failure. The 2026-07-30 cross-site RCA had to infer the dialed address from timing alone, which is what made a days-long partial outage expensive to diagnose rather than merely bad. The attribution is a VERDICT type over the chain, not a free-text log fragment: one token per outcome, single-sourced so the log sites and the tests that match them can never spell it differently. Critically, the post-lane-1 skip is its own verdict and never collapses into plain discovery: after REQ-ROUTE-CHAIN-FAILED-ADDR-SKIP a node that HOLDS a validated roster address can still fall to discovery because that exact address is the one its last dial failed on, and reporting that as discovery asserts the opposite diagnosis -- that this node has no roster address for the peer -- which would poison the #42/#49 relay RCA this observability exists to serve. The unresolved outcome splits the same way. This requirement owns only the REPORTING; REQ-PEER-ROUTE-CHAIN continues to own the chain order and this must never change an outcome. Gate: impl -- the leg verdict type and its single-source token, returned by the resolver and logged at the submit site, at the submit-refused failure, at the no-route failure, and carried to the drained DIAL_FAILED line (unrecorded means unknown, never a guess); unit -- each chain leg resolves to its own verdict on the existing chain rig, the just-failed roster address resolves to the SKIP verdict while a differing roster address still resolves to the roster verdict, and the unresolved cases split the same way. [OK] REQ-DIGEST-CROSS-NODE-PULL required: [doc, impl, unit, int] stages: +doc +impl +unit +int The digest SNAPSHOT PULL crosses nodes: `spt endpoint digest <[subnet:]id@node>` (including `--after ` incremental polling) resolves and serves under the EXISTING address gate — the CROSS_NODE_M4 qualified-address refusal lifts for the pull verb only. The structured-delta stream stays node-local until a real cross-node subscriber exists (ADR-0048 decision 5, defer-until-consumer; operator-ruled 2026-07-24). Access policy is NOT new — fetch was ratified address-gated long ago; this is routing. Composition note: REQ-DIGEST-SEAL-ON-IDLE's stable seqs are what make remote --after polling trustworthy. Forcing customer: spt-mobile 'view live digest of ANY endpoint' (host proxies transparently once the refusal lifts). Gate: int — two-node rig: a qualified digest pull from the non-hosting node returns the hosting node's buffer; --after returns only rows past the cursor; the address gate still refuses a non-resolvable caller. [OK] REQ-DIGEST-CURSOR required: [doc, impl, unit] stages: +doc +impl +unit -int `spt endpoint digest` gains incremental turn-end consumption (extends REQ-TERM-4/5): `--last ` = the last N TURNS (the digest's natural unit; --last 1 = the latest turn = turn-end output); a per-entry STABLE SOURCE-DERIVED `seq` in the --json output (deterministic from the entry's append position in the source — transcript record index across the session ledger / digest.log index — so a live re-projection yields the same seq for the same committed entry; NOT a window-position index that renumbers on slide); `--after ` = entries newer than seq still in the window (full window + signal if seq predates it, mirroring the version-slide full-refresh). An in-flight (still-growing) entry is flagged `partial: true` with NO stable seq until finalized (consumer reprocesses partial, skips <= seq). Also emit per-entry `ts` where present (seq is the authoritative dedup+cursor key). The digest's agent text is sufficient fidelity (no raw-source mode). BINDING doc-guidance: an adapter's [digest] extractor / api digest-entry MUST classify delivered user-facing messages as turn-opening `input` (equiv to direct PTY user-input), else messaging-driven sessions collapse into a few giant turns and --last/seq lose granularity. (v0.16.0) [OK] REQ-DIGEST-FETCHER-STRATEGY required: [doc, impl, unit, int] stages: +doc +impl +unit +int Bug #17 (W6b, closes eel-a end-to-end): [digest] gains a `fetcher` strategy mirroring [history]'s locate/normalize split (CONTEXT §history: [digest] mirrors history's two strategies — locate ownership). ROOT: the pre-W6b [digest] had only the locate_normalize analog (spt-core resolves ONE `source` template + pre-reads the file), which CANNOT express a PARTITIONED transcript layout — CC's projects//.jsonl or a date-globbed rollout tree — the exact case CONTEXT already assigns to the adapter. spt-core (correctly) provides NO {project}/slug key (harness-specific cwd munging = the charter violation FIX-A was rejected for). Fix: strategy = fetcher makes the ADAPTER's extractor locate + read + emit normalized records; spt-core runs it bounded (no locate, no pre-read, no stdin) and consumes stdout, feeding only the harness-NEUTRAL inputs it owns — {session_id}, the perch-bound {cwd} (info.json.cwd), and the captured [env] direction=read vars (W6/REQ-DIGEST-PROFILE-ENV) — so the extractor globs the unique {session_id} under {read-var-root}/projects/ with no slug. Keeps locate_normalize (default, back-compat) for a trivial single-file harness. Distinct capability from REQ-DIGEST-PROFILE-ENV (which supplies the root env). See docs/NEXT-MILESTONE-BUG-TRIAGE.md #17. [OK] REQ-DIGEST-GENERATION-SUPERSEDE required: [impl, unit, int] stages: -doc +impl +unit +int W3 (LIFECYCLE-TRUTH, digest projection truth — flynn filing spt-mobile d0aa3f4): a one-shot `endpoint digest --json` snapshot must return each logical activity row ONCE across a checkpoint/resume, not once per seq-generation. ROOT (spt-core-side, not a consumer bug): the K-session span (digest.rs activity_spanned, SPAN_SESSIONS=5) runs the [digest] extractor per session file and tags each row seq=(ledger_ordinal<<32)|localseq (REQ-DIGEST-CURSOR). A checkpoint/resume (self-/clear + Psyche rebuild) makes the harness REPLAY the prior generation's transcript into the NEW session file, so the ancestor's rows appear in BOTH the ancestor file AND the resume file at the SAME localseq — the span UNIONS them, one logical row surfacing under two full seqs (gen23,local208) + (gen25,local208), identical text/ts/localseq. Consumers dedup by exact seq (the documented authoritative key) so nothing collapses -> duplicate rows in every snapshot / `--after` view (`--follow from:0` is CLEAN — it reads current-generation only; the SPAN is the sole culprit). The trigger cannot disambiguate: `api boundary clear` records SessionTrigger::Clear for BOTH a fresh /clear (disjoint) and a carry-forward checkpoint (reporting.rs:94) — so a structural skip-ancestor needs new boundary metadata + adapter cooperation, deferred. FIX (doyle ruling — flynn Option 1 Supersede, projection-local, source-independent): within the span, collapse cross-generation replay dupes — an Activity record from an OLDER ordinal is dropped when an identical logical record (role, ts, text, tool) exists under a NEWER ordinal; keep the NEWEST-ordinal occurrence so the surviving seq is the live generation (snapshot + follow agree on seq). Cross-generation ONLY (never dedup within one ordinal — a session cannot replay itself; identical within-gen rows are real). Supersede runs on the raw span items BEFORE the window fold (project_timeline) so window_turns counts real turns, not phantoms; and a boundary divider adjacent to a now-fully-superseded ancestor is not left orphaned. Context entries (REQ-TERM-7, single digest.log) are not per-session-spanned -> untouched. Int = a two-session span rig where session B's extracted lines are a superset replay of A (same ts/text at same localseq) + B's own new tail: RED-first (pre-fix shows every A row twice); post-fix each logical row appears ONCE under B's generation, B's tail intact, the /clear boundary marker preserved when A retains rows. [OK] REQ-DIGEST-JSON-SELF-CONTAINED required: [doc, impl, unit] stages: +doc +impl +unit -int TEARDOWN-AUTHORITY W4 (perri adapter-surface finding 2026-07-19, doyle-grounded and RE-SCOPED; title AMENDED at gate 2026-07-19 — the original welded version to --after polling, todlando falsified it from digesthub.rs/cli.rs and doyle ruled the amendment rides the wave: fifth instance of the claim-keyed-on-the-wrong-thing class, this one in the REQ registry itself): `spt endpoint digest --json` must be self-contained on stdout. Today the digest snapshot version is NOT a field of the --json object at all — it exists ONLY in the DIGEST: version=N trailer that cmd_digest eprintln!s at cli.rs:1619 — so a JSON consumer that wants it is FORCED to parse stderr. NUMBER-SPACE TRUTH (the amendment): version is digesthub's monotonic PROJECTION counter — it bumps when the projected digest CHANGES, serves as the --follow from_version floor and a change-detection cursor, and is NOT valid --after input; --after filters on entry seq ((ledger_ordinal<<32)|line_idx), a different number space, so a version passed as --after predates the window every time. NOTE the corrected history (perri's original RCA framed this as a fleet outage and doyle falsified it; perri confirmed): the trailer has been on STDERR since it was added 2026-06-03 (16f4c8e) and no stdout trailer ever existed, so stdout-only consumers never choked — this is a CONTRACT-COMPLETENESS defect, low priority, NOT an outage. FIX: (a) emit version as a top-level integer field inside the --json object (field name pinned by the consumer — perri's adapter parses 'version' — so no second round is needed), alongside the existing after_predates_window signal; (b) gate the stderr trailer on the non-json path so --json leaves stderr clean while the human path keeps its status line. Result: --json stdout = pure self-contained JSON including the version, safe whether the consumer reads stdout-only OR merges 2>&1. Additive to the JSON shape (REQ-CLI-JSON evolution rule). The published doc must state the number-space split with the EXPLICIT NEGATIVE (version is NOT valid --after input) — the original title proves the misuse is the natural reading. Gate: doc — the json-shapes digest section carries version, the stderr-clean --json contract, the complete entry-kind enum with per-kind produced-vs-injected provenance, and the seq/version asymmetry incl the explicit negative; impl — version field on the --json snapshot path + non-json-gated trailer; unit — the --json object carries version and stderr carries no DIGEST: trailer, the non-json path still prints it (predates ordering included), and the behavior-change sweep confirms no existing test asserts the old --json shape or the trailer presence under --json. [OK] REQ-DIGEST-PROFILE-ENV required: [doc, impl, unit] stages: +doc +impl +unit -int Bug #17: spt endpoint digest returns NO_DIGEST for a ccs-profile endpoint (claude-spt:ccs) though [digest] is wired and the transcript exists — under .ccs (CLAUDE_CONFIG_DIR relocation) not .claude. The on-demand digest runs the extractor in the daemon context WITHOUT the endpoint profile transcript-location env, so the env-aware resolver cannot find the relocated transcript. Fix: propagate/persist the endpoint profile transcript-location env (e.g. the ccs CLAUDE_CONFIG_DIR) to the on-demand digest extractor so a profile-relocated transcript resolves; confirm the exact extractor verdict via spt adapter digest-proof. Ownership spt-core (digest env/profile propagation), possibly with a claude-spt extractor-resolver assist. See docs/NEXT-MILESTONE-BUG-TRIAGE.md #17. [OK] REQ-DIGEST-SEAL-ON-IDLE required: [doc, impl, unit, int] stages: +doc +impl +unit +int A digest turn's records gain their stable `seq` at the endpoint's IDLE TRANSITION — the adapter-reported, already-authoritative turn-finished signal — not at next-user-input arrival (which is demoted to a harmless no-op fallback seal). Kills the mint-then-idle deadlock (liam/spt-progress-tool W16: a finished-but-idle turn stayed partial/seq-less, so a seq-keyed scanner could never see the owner's latest turn until the owner was prompted again; interim drive-one-more-command workaround retires at ship). Sealing is IDEMPOTENT and SEQ-STABLE: it assigns seq to what the log holds at idle; a late-flushing straggler record folds into the sealed turn WITHOUT changing the assigned seq. (ADR-0048 decision 4; rejected: extractor-observed completion = banned heuristic inference class; provisional seqs = scanner bug factory.) Gate: int — the liam field shape RED-first: finish a turn, report idle, do NOT prompt again — the turn is sealed with a stable seq and a scanner keyed on seq sees it; fallback leg: next-input on an already-sealed turn re-seals nothing; straggler leg: a post-idle log record folds in, seq unchanged. [OK] REQ-DISK-FLOOR-PREFLIGHT required: [doc, impl, unit] stages: +doc +impl +unit -int A RIG OR GATE RUN MUST RECORD FREE SPACE AT RUN START, AND REFUSE TO START UNDER A FLOOR. MEASURED (IR-59): a full volume reds as a LINKER defect that names no disk -- LNK1318 Unexpected PDB error with a varying parenthesised code, beside LNK4209 debugging information corrupt, and once as a traceable-reqs check PANIC carrying os error 112, so even a REGISTRY red can be the disk's. Nothing in any of those failure texts says disk, two agents spent hours on a mechanism for one of them, and neither read free space. THE FALSIFIER IS FREE SPACE AT THE TIME OF THE RUN, not the shape of the leg table, and a reading that appears only on refusal leaves every red already in hand uninterpretable -- which is the state the fleet was in, since local gate legs run as plain per-run scripts with no preflight path to amend. So the reading is printed on EVERY run, passing or not, in the golden runner's own key shape (disk preflight: drive= free_bytes= floor_bytes=, and RESOURCE=disk on refusal) so one grep reads a local rig log and a CI log alike, and an optional label attributes readings on a box carrying several lanes. The default floor is the golden runner's 32 GiB, so a local rig and a golden leg refuse at ONE number rather than two; a floor of ZERO is print-only mode for informational call sites and never refuses, which is why the default arm must still refuse the same reading a zero floor admits. A run that starts under the floor produces reds that belong to nothing: they are re-run, never re-read. AND ONE READING IS AN INSTANT, NOT HEADROOM (IR-46): the danger window is the TAIL of a cold build -- the measured instance died linking the last and largest artifact of a 74 GB pool while that same build drained the volume out from under itself, and the reading taken before it looked fine. [OK] REQ-DISPATCH-CLAIM-RETRY required: [impl, unit, int] stages: -doc +impl +unit +int REDISPATCH-TRUTH W1 (ADR-0038, hertz fix C): dispatcher claims are RETRYABLE and outcomes are CLASSIFIED — today claimed.insert(stream_id) happens PRE-spawn (dispatch.rs:206/214) and is never cleared or retried on Unknown/Failed, so one transient worker-setup failure permanently abandons the stream. Distinguish active/retryable vs served vs finished: a transient worker-setup failure releases/requeues the claim (bounded/backoff-shaped); terminal classification outcomes stay terminal and do NOT hot-loop (show the distinction in test, not just code). Gate: impl — claim lifecycle; unit — transient failure requeues + terminal outcome does not (no hot-loop under a persistently-failing stream); int — production-path regression D3: inject ONE transient worker-start failure, prove claim retry recovers the stream with NO duplicate controller/output. HEAVY nextest group at birth. Kin REQ-REDISPATCH-FINISHED-RETIRE, REQ-STREAM-OPENER-DURABLE. [OK] REQ-DISPATCH-FALLBACK-CIRCUIT required: [doc, impl, unit, int] stages: +doc +impl +unit +int REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 1+5): the opener ring-peek fallback fires ONLY on the explicit UnsupportedVerb/old-broker answer — transport timeout/EOF/poison classify Failed and requeue bounded, NEVER a second replay subscriber (today first_line's Err(_) arm at dispatch.rs:414 catches everything; the comment intends old-broker-only). Retries are CLASSIFIED: pre-setup transient may retry; a deadline-poisoned replay is CIRCUIT-BROKEN (global backoff) and a replacement subscriber is never installed until the prior subscriber is fully gone. NOT a revert to the v0.33 burn-the-claim abandonment (rejected): the stream must recover after the breaker window — show it in test. Gate: impl — narrowed fallback arm + breaker; unit — timeout/EOF/poison never reach the peek path while unsupported-verb does + breaker trips and resets + no-reinstall-until-gone; int — T2 (no peek subscriber created on transport errors) + T5 (breaker recovery, the not-abandonment discriminator) + T7 mixed-image N-1 with REAL traffic-carrying streams (handshake-only insufficient); doc — ADR-0038 Amendment. Kin REQ-DISPATCH-CLAIM-RETRY (upgraded, not reverted), REQ-STREAM-OPENER-DURABLE (the N-1 window it narrows). [OK] REQ-DISPATCH-HYGIENE-TELEMETRY required: [doc, impl, unit] stages: +doc +impl +unit -int REDISPATCH-STALL W1 (ADR-0038 Amendment, fixes 7+8+9): dispatcher hygiene + keyed observability — (a) bounded redispatch worker pool with batched cold enumeration, NO claim locks held during I/O (today thread::spawn per stream, unbounded on a cold table); (b) sessions-lock discipline: clone-then-drop before detach_if/info.json I/O (KH 7.12 kin — no fs/conn I/O under global locks); (c) keyed stage telemetry: gen/stream/family/endpoint/attempt/conn on every dispatch record + replay/poison/cancel/worker-outcome events + gauges + PTY high-water/RC cursor — the field-discriminator surface (poison-window census, stream-sub-attach per generation dropping to O(active streams)). Gate: impl — pool + enumeration batching + lock discipline + telemetry keys; unit — pool bound honored under a cold flood + no-lock-across-I/O seam + telemetry key completeness; doc — ADR-0038 Amendment. Kin REQ-HAZARD-REDISPATCH-STALL, KH 7.12 (locks), REQ-CONN-POISON-ATTRIBUTION (extends its attribution). [OK] REQ-DOC-DELIVERY-VOCAB required: [doc] stages: +doc -impl -unit -int W6 (LIFECYCLE-TRUTH, docs — remaining flynn/perri gaps folded): publish the full send-outcome vocabulary (SENT / SENT(WAN) / QUEUED window semantics / DEFERRED / NO_PERCH), digest --json row schema, api poll auth + MAC-stamp prefix, remaining --json shapes checklist (seed #3). Public docs use VERSION numbers, never wave codes; docs-publish drift gate applies. [OK] REQ-DOC-ECHO-COMMUNE-CONTRACT required: [doc] stages: +doc -impl -unit -int W6 (LIFECYCLE-TRUTH, docs — this gap cost a full outage night, priority slot): publish the [session.echo_commune] I/O contract on the docs-site: key catalog core fills; core does NOT stdin-feed [history] (field-proven); self-locate guidance incl. CLAUDE_CONFIG_DIR / read_env; drop-file protocol (single-writer, ingest-deletes, resolver semantics from W1); stdout ingestion expectations. Public docs use VERSION numbers, never wave codes; docs-publish drift gate applies. [OK] REQ-DOC-ENDPOINT-DROP-RESOLUTION required: [] stages: -doc -impl -unit -int D1 (F028, perri F-c; docs/truth): SI-1's resolution rule — a RELATIVE watched drop dir resolves against the ENDPOINT's cwd, never the daemon's (KH 7.28, shipped v0.22.0) — is documented NOWHERE public. Add it to harness-contract/manifest.md + the manifest schema field descriptions so an adapter author knows a relative commune_dir/signoff_dir is endpoint-resolved. docs-drift gate applies. See triage D1. [OK] REQ-DOCS-1 required: [doc, impl] stages: +doc +impl -unit -int Dual-audience docs (human + AI dev-agent), markdown once / two depths [OK] REQ-DOCS-2 required: [doc, int] stages: +doc -impl -unit +int Sub-10-minute runnable killer quickstart per audience [OK] REQ-DOCS-3 required: [doc] stages: +doc -impl -unit -int Diátaxis structure; one canonical way to do X [OK] REQ-DOCS-4 required: [doc, impl, unit] stages: +doc +impl +unit -int Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help) [OK] REQ-DOCS-5 required: [impl, int] stages: -doc +impl +unit +int Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked [OK] REQ-DOCS-6 required: [impl, unit, int] stages: +doc +impl +unit +int spt how-to : in-binary task-oriented agent instructions (anti-drift; quickstart prompts point agents at it) [OK] REQ-DOCS-CHANGELOG-PAGE required: [doc, impl, unit] stages: +doc +impl +unit -int THE CHANGELOG IS A PAGE OF THE LOCAL DOCS SITE, GENERATED FROM THE REPOSITORY CHANGELOG AND NEVER HAND-EDITED (releases#268, WEBSERVE W3). A user reading the node-local docs must be able to answer what changed in this release without leaving the site for a repository file they may not have. GENERATED, NOT COPIED: a pure changelog_page renderer writes docs-site/src/changelog.md from CHANGELOG.md, xtask gen writes it and xtask check diffs it, exactly as the CLI reference page already works -- so a changelog edit that is not regenerated is a CI red rather than a silently stale page. THE GENERATOR STRIPS EVERY INTERNAL-CODE CLASS THE PUBLISHED-DOCS SCAN FORBIDS, KEYED ON THE CLASSES AND NOT ON TODAY'S POPULATION: the traceability doc-tag comments CHANGELOG.md carries for its own coverage, and the parenthetical private-tracker citations in its prose. MEASURED 2026-09-07 at the W3 lane base: five doc-tag comments AND two prose citations of the tracker, the second class absent from the brief and sufficient on its own to red the scan on run one; no decision-record or milestone-wave token is present today, and the stripper covers those classes anyway because the next entry an author writes is free to add one. THE PAGE JOINS THE PUBLISHED-DOCS SCAN'S PATH LIST rather than growing a second mechanism, and it is LISTED IN THE SUMMARY AND THE CURATED llms index, which is also what puts it in the full export -- the export reads pages off disk in summary order, so the page is a checked-in generated file and never a render-on-request route. Gate: doc -- the changelog page present in the docs site with its summary and llms rows; impl -- the pure renderer, its gen write, its check diff, and its row in the internal-codes scan list; unit -- the renderer strips each forbidden class, leaves ordinary prose byte-identical, and the rendered page carries no token the scan forbids. [OK] REQ-DOCS-LOCAL-SERVER required: [doc, impl, unit, int] stages: +doc +impl +unit +int THE-FORKENING W3 (ADR-0036 §4, operator-ruled crate-over-handroll + port 5474): the daemon (broker side — docs are up whenever the daemon is) serves $SPT_HOME/docs over HTTP on LOOPBACK ONLY, default 127.0.0.1:5474 (+ ::1 where available; 547 rejected — privileged <1024 breaks the Linux user-daemon + IANA dhcpv6-server), config + env override. Implementation = hyper (ALREADY in-tree via iroh/reqwest — zero new supply chain; operator ruled battle-tested crate over hand-rolled HTTP). GET-only; strict path-sanitize under $SPT_HOME/docs (reject .., absolute, encoded traversal); bounded write discipline (conn-hazard class). Published URL surface preserved VERBATIM (llms.txt contract: /llms-full.txt, append-.md raw, /manifest.schema.json — flynn/perri doc habits must not break). Verbs: `spt docs url` prints the resolved URL honoring overrides; bare `spt docs` opens the system browser (cfg(windows) creation_flags no-console). NEVER 0.0.0.0 — a LAN-visible server re-leaks what privating hid. Gate: unit — path-sanitize rejections + content-type map + loopback-only bind config; int — daemon up serves book index + llms-full.txt + a raw .md byte-true from a landed bundle; doc — CONTEXT.md Docs Server entry + docs-site self-reference updated (site-url). Kin REQ-DOCS-RELEASE-ASSET (the producer), ADR-0036, translation-binary console-window gotcha. [OK] REQ-DOCS-NO-INTERIOR-SPACE-RUNS required: [impl, unit] stages: -doc +impl +unit -int Operator-facing text carries no INTERIOR run of 3+ spaces, judged on RENDERED bytes rather than source text. The defect is a Rust line-continuation backslash that a generator or a careless edit ATE: the literal still compiles, still passes every suite, and only the rendered message is wrong, so no gate we run can see it and only a diff read ever has. The check must RENDER, because a HEALTHY continuation puts a real run of spaces in the file on the next line every time (releases#74 own instance carries thirteen and renders with one) — a source-text scan therefore flags every correctly-continued literal in the crate, and a check with that false-positive rate is one somebody switches off. SCOPE (doyle, re-ruled on the #208 evidence): IN = CLI help and CLI stdout/stderr AND daemon-emitted operator diagnostics, one surface wider than the original brief because the five literals that re-opened the class lived in daemon diagnostics; OUT = test assertion messages, read by someone already reading a trace. A deliberate run stays possible via a per-site greppable `spacerun-ok: ` marker carrying a REASON TOKEN, so an exception is a decision written down rather than a hole the gate cannot see; per-site is ruled, because an idiom-aware exemption would have to DETECT alignment, alignment is fuzzy, and any rule recognising `this looks aligned` eventually exempts an eaten continuation that happens to look aligned — a silent false negative in the one direction this check exists to prevent. THIS REQUIREMENT CLAIMS INTERIOR RUNS, NOT THE CLASS (doyle, 2026-08-22). A continuation dies two ways and only one is in reach: if the backslash is deleted AND the lines are joined, the indentation lands mid-sentence and is refused — the JOIN variant, which is measurably what 3416ffd3 repaired (pre-fix run = 18 spaces, those bytes now a cell); if the backslash alone is deleted, the literal keeps its newline and the indentation arrives as LEADING whitespace, which is NOT refused, deliberately, because a help block legitimately indents every line. The limit is named in the module doc as well as here so that greping this id cannot be read as the class being closed. [OK] REQ-DOCS-NO-INTERNAL-CODES required: [doc, impl, unit] stages: +doc +impl +unit -int Public CLI --help (the clap `///` doc-comments) and the generated `docs-site/src/cli/reference.md` MUST NOT contain internal tracker/decision codes — `REQ-*`, `F-###`, `M#-W#`, `ADR-####`. They are meaningless to an end user reading --help and ship to GH-Pages. A CI-gated scan (the `xtask check` docs gate) fails on any such token in the GENERATED reference.md (which by construction contains only clap help, so rustdoc `///` on fns/structs is OUT of scope and keeps its REQ/ADR cross-refs). Substance is kept; codes are rewritten to plain language. (v0.13.2 W6) [OK] REQ-DOCS-RELEASE-ASSET required: [doc, impl, unit, int] stages: +doc +impl +unit +int THE-FORKENING W2 (ADR-0036 §4): every release ships a platform-independent docs bundle `spt-docs.tar.gz` (BUILT mdbook output: HTML + llms.txt + llms-full.txt + raw .md + manifest.schema.json) as a release asset WITH an entry in the SIGNED update-set (sha256, same integrity chain as binaries — docs describe the security-relevant contract surface, they do not ride unverified). Apply lands/refreshes $SPT_HOME/docs (single current copy = docs always match the installed binary). FAILURE ISOLATION binding: a docs-asset failure NEVER fails the binary update — UPDATE_DOCS_SKIPPED loud, retried next fetch. Gate: unit — update-set entry + sha256 verify + skip-loud isolation; int — a fetch+apply lands version-matched docs at $SPT_HOME/docs; doc — self-update docs name the bundle. Kin REQ-DOCS-LOCAL-SERVER (the consumer), REQ-RELEASE-CHANNEL-PRIVATE (the assemble leg), ADR-0036. [OK] REQ-DRIVEN-BY-OWN-NODE-NORMALIZE required: [doc, impl, unit] stages: +doc +impl +unit -int RC-RENDER-TRUTH v0.38.1 fast-follow leg 1 — RESHAPED by operator/hertz correction + CONTEXT.md:382-389 grounding (doyle ruling v3, 2026-07-19; the original own-node NORMALIZATION is WITHDRAWN): plain `spt rc` on a CONTROLLED endpoint is refused-with-guidance BY DESIGN (CONTEXT:386 — never silent-displace; --take is the opt-in kick), and `driven_by` = the CONTROLLING NODE including the own node (CONTEXT:386 single-writer datum — the stamp comment's remote-only claim was a legacy-path artifact, NOT the model). v0.38.0 shipped behavior is CORRECT: the own-hex latch + client guidance blocking a second same-node plain rc IS the documented refusal, and the W2 generation ladder stays reachable exactly where it belongs — the recovery seams (rc reconnect re-drive + dispatcher re-serve bypass the client gate; the hertz field FAIL leg is reclassified NOT-A-DEFECT). Remaining work = truth/cosmetics across ALL own-node display surfaces (doyle gate finding 2026-07-19 — the own-hex latch is truthful but only rc.rs first learned to HUMANIZE it; the picker pin + endpoint-info attached_node still read the own-hex stamp as a foreign remote driver and print raw hex): (a) rc.rs guidance copy names the actual state — controlled from another window on this machine — via a pure driver_phrase keyed on the SHARED own-node predicate (roster::is_own_node_hex); (b) picker control_line (view.rs) reads an own-node driver as controlled locally (driven_by_is_self computed at data-build where the raw hex is available, driven_by STAYS populated so View+Kick is still offered); (c) endpoint-info attached_node (reporting.rs derive_attached_node) self-attributes an own-node driver to THIS node, never a foreign null-label hex; (d) stamp_driven_by + set_driven_by/set_controlled comments rewritten to the CONTEXT:386 meaning (controller node, own included) + KH 7.15/secondary remote-only model-claim comments reconciled (selfheal/unlatch semantics unchanged) — STAGE-NOTE CORRECTION (todlando 2026-08-19, doyle-ruled): this item shipped only its FIRST half. The WRITER-side comments were rewritten (broker.rs stamp_driven_by, info.rs set_driven_by, info.rs set_controlled all carry the ruling and are the anchors). The `secondary remote-only model-claim comments reconciled` half was NOT executed: the InfoJson FIELD docs for driven_by/controlled, picker/model.rs's controlled field doc (which contradicted driven_by_is_self twelve lines above it in the same struct), and the REQ/triage records that restate the falsified model all still carried remote-only. Executed by the sweep commit that carries this correction. No prod behavior site ever encoded remote-only — display_status keys on driven_by || controlled, correct under both models — which is why the stale premise never shipped a bug; (e) plain-Control-on-controlled refusal + --view/--take bypass pinned for BOTH remote and own-node drivers. NO broker behavior change; ladder untouched; pure display (no int, same as the ruling-v3 dropped int). Gate: impl — rc/picker/endpoint-info humanization + shared predicate + comment/docs; unit — own-node latch stamps truthfully (broker), guidance fires for own+remote with the right copy (rc), driver_phrase names own-node plainly (rc), control_line reads own-node as controlled locally (view), attached_node self-attributes own-node (reporting), --view/--take bypass; doc — KH 7.15 + ADR-0044 consequence-note alignment. [OK] REQ-DSR-SINGLE-CPR required: [impl, unit] stages: -doc +impl +unit -int TEARDOWN-AUTHORITY W3 rider (hertz RCA 2026-07-19): a Device Status Report query yields EXACTLY ONE Cursor Position Report. Rides W3 because it is the same emulator-conformance surface as the width work and the same field capture surfaced it; kept a separate REQ so its evidence is not buried inside the width tags. Gate: impl — one CPR per DSR on the emulator reply path; unit — a DSR in the input stream produces a single well-formed CPR carrying the DISPLAY-column cursor position (i.e. consistent with REQ-SCREENGRID-WIDTH reckoning, not the raw char count). [OK] REQ-ECHO-BOUNDARY-INPUT-BEFORE-ROTATION required: [doc, impl, unit] stages: +doc +impl +unit -int A SESSION BOUNDARY FIRES AN ECHO WHOSE INPUT IS CAPTURED BEFORE THE SESSION ID ROTATES (releases#113 fork 3, ruled 2026-08-30 off the measurement it ordered). MEASURED at 343df76e: spt-core caught NO delta at a boundary, on three independent arms. (1) cmd_boundary fires no echo and arms no gate -- it rotates the sid, carries an engine-room empowerment, re-stamps ready, appends the session ledger, publishes one IO boundary event, nudges the project index and resurfaces notifs; the three production fire_echo triggers are pulse, rest-transition and signoff, and a boundary is none of them, nor does the departing session's soft api session-end fire one. (2) Even a fire placed after a boundary could not SEE the departing work: fire_echo feeds history(session_id) resolved through the CURRENT sid, and [history] resolves a single {session_id} file -- ADR-0019 states that property in as many words -- so the input after a rotation is the new, empty session. Session-spanning exists in this codebase but belongs to the DIGEST (ledger + last-K, REQ-TERM-6); the echo does not read it. (3) No last-echo watermark exists anywhere (zero hits across crates/), so the delta since the last echo is not a quantity spt-core holds. THE CONSTRAINT IS INPUT-BEFORE-ROTATION, and the shape delivering it (fire pre-rotation, or snapshot the input then rotate) is the implementer's. NO WATERMARK THIS PHASE, ruled: the whole current-session input stands -- it is a SUPERSET of the delta and the Psyche ingest consumes communes as content-deltas anyway; delta-since-last-echo semantics would be its own request. The boundary echo must not be able to FAIL the boundary: the harness has already rotated by the time core is told, so an echo problem is loud and best-effort, never a refusal that desyncs spt's record from the reality it describes. [OK] REQ-ECHO-BRIEF-DIRECT-ROUTE required: [doc, impl, unit, int] stages: +doc +impl +unit +int THE ECHO-COMMUNE BRIEF ROUTES STRAIGHT FROM THE SUMMARIZER INTO THE TWO-TIER STORE AND NEVER TRANSITS THE AGENT `-commune.md` DROP PATH (operator-ruled 2026-09-06, releases#276). MEASURED BEFORE THE RULING, on doyle own funnel: every `/clear` that hour was followed by an echo frame 25-45s later, landing exactly in the window where an unread Self drop sits, and 2 of 3 Self communes were lost -- the reborn session reported its own wake text as stale. WHY THIS IS A DESIGN GAP AND NOT A RACE TO PATCH: CONTEXT.md:208 names the drop as the AGENT channel and KNOWN-HAZARDS 6.4 names ONE writer on that path plus the daemon as sole deleter; echo.rs read `the mind` as the Psyche and forgot the Self, while docs/CONTEXT-MEMORY.md:25-33 states the Self DOES write that path at a boundary and that the Self signal is the STRONG live-context signal. So the echo -- weak, project-primary, live-conservative -- overwrote the strong Self capture: the exact inversion of the tiering the provenance asymmetry exists to protect. Nothing in the design ever required the brief to transit a file; the file was an implementation convenience that re-used the ingest. THE SHAPE: `run_echo_commune` loses `commune_dir` and returns the provenance-stamped body; the body-to-tiers core of `ingest_drops` is factored into ONE shared `ingest_body` so two callers cannot fork a later tiering change; `fire_echo` calls it, publishes the COMMUNE io-event carrying verbatim what ENTERED the tier (echo header first), consumes the releases#96 intent marker at THAT line rather than at the pulse file sweep, and strikes HARD on a route error through the existing EchoError classification. BEHAVIOUR CHANGE THAT MUST BE STATED IN THE PR: an adapter declaring `[session.echo_commune]` with NO `commune_dir` now echoes, where it used to no-op. Gate: doc -- the harness-contract echo-commune page rewritten to state the direct route, plus the CONTEXT.md:208 and :259 sentences; impl -- echo.rs without the write, ingest.rs `ingest_body`, and the fire_echo route-publish-consume order; unit -- the returned body carries the `Source: echo-commune` header and no drop dir survives in the signature; int -- through a real daemon the COMMUNE io-event appears with that header and NO file appears in `commune_dir`. [OK] REQ-ECHO-DROP-DIR-RESOLVE required: [impl, unit] stages: -doc +impl +unit -int W1 (LIFECYCLE-TRUTH): fire_echo resolves the manifest commune_dir through the SAME resolver its siblings use before any write. ROOT (pinned): fire_echo (spt-daemon lifecycle.rs:790) passes the RAW manifest commune_dir into run_echo_commune -> echo.rs:115-117 create_dir_all+join; a relative `.claude` under the WMI-launched daemon's System32 cwd = os error 5 deterministic (two live psyches stamped FAILED on it). Siblings already resolve correctly (ingest ~:583, psyche_drop_file :1072 via resolve_endpoint_drop_dir(raw, cwd)). FIX: fire_echo routes through resolve_endpoint_drop_dir; relative-with-no-cwd = SKIP LOUD (stderr), never a raw relative write — kills the latent-worse variant where a writable daemon cwd writes the drop to a WRONG dir silently (echo communes lost, no error). Hardening riders (same touch, no separate REQ): bounded EACCES retry on the drop write; echo claude spawn gets explicit cwd = endpoint cwd (perri ask). [OK] REQ-ECHO-IDLE-AGE-GATE required: [doc, impl, unit] stages: +doc +impl +unit -int THE ECHO GATE SPLITS BY ARM INTO TWO SENTINEL FILES, AND ONLY THE WORK-DRIVEN ONE IS AGE-GATED (releases#113 fork 1, re-ruled 2026-08-30 on a corrected premise). MEASURED FIRST, AND THE MEASUREMENT INVERTED THE TICKET: the W0 report recorded the gate as armed by TRANSITION EDGES ONLY (arm_transition_echo) and concluded that an agent working for hours without a transition edge never echoes. FALSE -- cmd_state's idle arm (api/delivery.rs) writes the same sentinel UNCONDITIONALLY unless --no-gate, and that is published contract (harness-contract api.md: reporting idle also arms the echo gate). `api state idle` IS the turn-end report, so a working agent arms at EVERY turn end and the pulse loop fires within one period. spt-core was never missing the work-driven trigger; it was missing the AGE GATE. THE RULED SHAPE IS TWO FILES, NOT A CARRIER INSIDE ONE: (a) the EDGE sentinel, written by arm_transition_echo, fires UNGATED as today -- detach / attention-shift / suspend are rare and attention-correctness wants timeliness, and gating them would drop exactly the fire that matters at detachment risk; legacy supports the split, its boundary and session-edge fires were not age-gated either. (b) the IDLE sentinel, written by the idle arm, fires only at AGE >= 15 MINUTES, carrying the three legacy mechanics: a fresh sentinel answers SkipSentinelFresh{remaining} and the caller schedules a SHORT NEXT PULSE to wake near the crossing (this file only); age reads BIRTHTIME with an MTIME FALLBACK and the degradation is DECLARED rather than silent (birthtime = oldest un-fired turn end, mtime = most recent); and a metadata failure or clock skew FAILS OPEN -- it FIRES. Two files rather than one carrier is the property: an idle write can never overwrite an armed edge (no last-writer-wins downgrade), each file has one writer class, read-and-clear stays per-file, and the fire-time distinction is STRUCTURAL rather than parsed out of a byte someone has to remember to write. THE ARMING CONTRACT IS UNCHANGED and the docs sentence stays true -- reporting idle still arms; what acquires the age discipline is the FIRE. [OK] REQ-EFFECTIVE-INSTANCE-STATE required: [doc, impl, unit] stages: +doc +impl +unit -int A-1 (REMOTE-TRUTH triage §A + ADR-0033 §Decision): the effective instance state of a perch is DERIVED through ONE shared function — liveness discriminates warm/cold, stored rest intent refines within warm, absent intent NEVER defaults active. ROOT (certain): resting::apply_event derived its `from` off the stored rest_state field ALONE (resting.rs:225, `unwrap_or(RestState::Active)`) — a cold perch (offline) with no intent answered `from=Active`, so a Wake event found it 'already in target state' and returned Ok(None) = the field NO_EDGE-on-a-definitely-suspended-endpoint bug (the banked F-028 rest_state-void seed). advertised_status (registryhost.rs:821) ALREADY derived correctly (is_perch_alive→intent-refined / is_perch_unbound→Dormant / cold→Suspended) — the two readers disagreed. FIX (Q1 shared derivation, hazard-class): a pure `effective_rest_state(alive, unbound, intent) -> RestState` mirroring advertised_status, consumed by BOTH advertised_status (mapped RestState→Status, behavior identical) AND apply_event's `from` (real is_perch_alive/is_perch_unbound reads); void + cold ⇒ Suspended. Bonus: kills the spurious active→suspend echo a cold+void perch used to fire (on_rest_edge on a dead driver). Red-first: perch status=offline + no rest_state → daemon_rest_event(Wake) yields from=Suspended→to=Active EdgeReport, not Ok(None). [OK] REQ-ELEVATE-1 required: [doc, impl, unit] stages: +doc +impl +unit -int Cross-platform self-elevating re-launch for privilege-gated commands: a pure decision seam `decide_elevation_path(os, elevation, interactive_tty, has_display, has_pkexec, has_term_emulator) -> ElevatePath{AlreadyElevated, InlineSudo, UacWindow, Pkexec, TerminalEmulator, PrintHint}` selecting how to re-acquire privilege, and the per-OS impure launchers it dispatches — Windows UAC console (ShellExecuteW `runas` on the abs-exe + verbatim argv; the elevated child does the work, prints 'You can close this window', and pauses for a keypress; the original prints 'Elevated terminal launched…' and exits 0; NEVER pipes the child's stdout back across the privilege boundary), Linux desktop pkexec (preferred, native polkit GUI auth) else x-terminal-emulator -e sudo (fallback list x-terminal-emulator→gnome-terminal→konsole→xterm), the existing interactive-TTY inline sudo, and the headless/no-path floor that prints the absolute-path command. Reused by every gated command (not subnet-specific). Generalizes should_auto_elevate. [OK] REQ-EMIT-SINGLE-WRITE required: [impl, unit] stages: -doc +impl +unit -int A machine-parsed diagnostic emission is handed to the OS as exactly ONE write of the complete rendered text including its terminating newline. [OK] REQ-ENDPOINT-AUTOSTART required: [doc, impl, unit, int] stages: +doc +impl +unit +int MSG-IDENTITY W5 / F-038 (flynn operator-directed ask 2026-07-10, SPT-CORE-NEEDS #7 + deployah field-confirm same day: mobile-gw alive=false after the v0.30.6 full daemon restart = this feature's absence, live): an endpoint can be marked a STARTUP DEFAULT so the daemon brings it back up at daemon start — Gateway-class endpoints are infra (the phone treats mobile-gw as always-there; box reboot / daemon cold start currently leaves it down until hands-on). SHAPE RULED (doyle, dispatch): (a) `spt endpoint auto-start ` persists the endpoint (id + adapter/profile + args) as a startup default REPLAYED at daemon start, symmetric with the shipped `subnet attach/detach --save` precedent — smallest orthogonal cut, explicit operator intent, no interaction with effective_rest_state/F-035 reader-parity semantics (shape (c) restore-what-was-up REJECTED for now: principled but couples to the rest_state neighborhood that just churned; revisit if --save proves insufficient in the field). A saved endpoint that fails to come up logs loud + does not block daemon start or other replays. flynn docs sweep confirmed missing-feature not docs-gap (rest/wake manual-only; no endpoint analog of subnet --save; no manifest field; no api surface). Gate: int — daemon restart brings a --save'd endpoint back up (fresh daemon, saved default, endpoint reaches its steady state without hands-on); doc — public docs page for the verb (VERSION-scoped); unit — persistence round-trip + replay skip-on-missing-adapter loud. Kin subnet --save (the symmetry precedent), REQ-LIST-JSON-LIVENESS-PARITY + REQ-HAZARD-BIND-REST-STATE-CARRY (the F-035 neighborhood shape (c) would have coupled to), [[spt-core-findings-backlog]] F-038. Interim on flynn's box (logon scheduled task) dissolves when this lands. [OK] REQ-ENDPOINT-CYCLE-HONEST required: [doc, impl, unit, int] stages: +doc +impl +unit +int REGISTRY-LIFECYCLE W3 (ADR-0041 decision 6, operator deployah stop/run wedge): cycle verbs share ONE liveness authority — the ALREADY_LIVE dup-guard liveness-probes the claimed session client tree before refusing (dead tree means reap + respawn honestly, never a refusal citing a zombie); the shutdown state machine consults the same source so is-it-live has one answer (no ALREADY_LIVE / list-OFFLINE / shutdown-NO_EDGE three-way contradiction on the same endpoint). Gate: impl — probing dup-guard + unified authority; unit — dead-tree claim probes and reaps, live claim still refuses; int — controlled zombie (killed client tree, surviving hosted record) leads to endpoint run succeeding honestly end-to-end; doc — ADR-0041. [OK] REQ-ENDPOINT-DROP-DIR-SURFACE required: [doc, impl, unit] stages: +doc +impl +unit -int THE REGISTERED COMMUNE DROP DIR IS READABLE FOR THIS NODE'S OTHER ENDPOINTS, NOT ONLY FOR SELF (releases#224, scope amended at measure-first): `endpoint list --json` HAS carried `drop_dir` on the SELF pin since 2026-08-01 (REQ-PSYCHE-INGEST-FAILURE-LOUD, `registered_drop_dir` resolving through the SAME `spt_live::resolve_endpoint_drop_dir` the daemon ingest anchors on -- one anchor, so the read surface cannot drift from the watcher), and the ticket's premise that no read verb exposes it is FALSE at head and at the shipped 0.62.0 binary; the residual is that the field appears EXACTLY ONCE in the whole roster JSON. The field census's real question is about ANOTHER agent's endpoint -- an orphaned drop is diagnosed by whoever finds it, not by its owner -- so `LocalPerchJson` rows gain `drop_dir` on the SAME terms as their `activity` and `host_error` siblings: read from that perch's own `info.json`, LOCAL ROWS ONLY, additive and `skip_serializing_if` so an endpoint with no recorded cwd/adapter or a manifest declaring no commune dir OMITS the key rather than guessing (an absent field is honest; a guessed dir is worse than silence). SUBNET-REMOTE ROWS (`EndpointRowJson`) STAY BARE (doyle ruling 1): a remote endpoint's drop dir is another NODE'S filesystem fact, and surfacing it here invites acting on a path this machine cannot read. The HUMAN view gains the same fact for SELF -- one annotation line under the self pin, the `psyche-host:` / `input-translation:` family -- so the answer does not require `--json`. Gate: doc -- the published drop-file-protocol clause telling an agent to READ the resolved dir rather than re-derive it, naming both JSON fields and the human line (the DISCOVERABILITY half of the residual: nothing pointed at the shipped field, which is how a multi-round probe happened over a fact that was already there, and how the ticket came to be filed on a false premise); impl -- the local-row field + the human self-pin line, both sourced through the one resolver, never a second derivation; unit -- a local row with a resolvable dir CARRIES it while a row with no cwd/adapter OMITS the key (the pair asserted together, since either alone passes an implementation that is uniform the wrong way), the remote row shape is UNCHANGED (the ruled boundary, pinned so a later widening is a decision and not a drift), and the human self pin renders the dir when known and renders NOTHING extra when not. [OK] REQ-ENDPOINT-LIST-JOINED-SUBNETS-LABEL required: [doc, impl, unit] stages: +doc +impl +unit -int THE THIS-NODE SUBNET LINE IN `spt endpoint list` SAYS JOINED, NOT SHARED (releases#72, BUGFIX). The two subnet lines this surface renders are DIFFERENT FACTS wearing one label: the This-node line lists the subnets THIS node is a member of (cmd_endpoint_list names, narrowed by --subnet), while a remote node row lists the subnets that node gossips a visible row through UNIONED ACROSS OUR MEMBERSHIPS -- the subnets we have in common with that machine. Calling the first one Shared invites the reader to look for a second party that is not there. THE REMOTE LINE KEEPS ITS NAME and that is part of the requirement rather than an omission: Shared is precise for a row whose content IS an intersection, and renaming both would trade one imprecision for another. The render docs already carry the corrected vocabulary (render_node_grouped is documented as carrying our joined subnets for this node and a Shared subnets line for the remotes), so this change makes the rendered text agree with the language the code already uses for it. THE UNIT EVIDENCE MUST DISCRIMINATE THE TWO LINES, which the pre-fix assertion could not: its fixture gave the This-node set and the remote set the SAME members, so a substring assertion matched either line and would have passed unchanged while the subject of the assertion silently changed. The repinned row asserts each label once, against fixtures whose two subnet sets DIFFER, so a build that renames the wrong line fails it. Gate: impl -- the This-node line renders Joined subnets while the remote-group line is untouched; unit -- both labels asserted by name with distinct fixtures, so neither line can be renamed, dropped or duplicated unobserved. [OK] REQ-ENDPOINT-LIST-MERGE-LOCAL required: [doc, impl, unit] stages: +doc +impl +unit -int `spt endpoint list` always merges this node's LOCAL (unadvertised) perches into the view; the `--local` flag is REMOVED (operator decision 2026-06-17). Rationale: `spt whoami` is a thin alias of `endpoint list` — a just-online agent running `whoami` must see its OWN perch, or it gets an omitted-self view ('chaos'). FIX: drop the `--local` flag + its `--detail` conflict test + the v0.10.0 REQ-PICKER-5 hint line (cli.rs:1678) + cmd_list_local; the bare list merges local perches into the subnet view; fix the whoami alias path accordingly. Run `cargo run -p xtask -- gen` (docs-drift, DEFAULT target). (v0.12.1) [OK] REQ-ENDPOINT-LIST-NODE-GROUPED required: [impl, unit] stages: -doc +impl +unit -int spt endpoint list is node-grouped over unique INSTANCES, not subnet-grouped over duplicated rows: one row per (id,node) instance — subnet duplication collapsed (ADR-0006 §1: subnet is never an identity axis), freshest-epoch wins a cross-subnet status disagreement (the resolve_among twin rule) — grouped under 'This node: