required_stages = ["impl", "unit"] # ACTIVATED 2026-09-06 (v0.38.0, claude-spt-bs#26). impl: hook.rs `arm_from_commune_frame` called from handle_stop after the disk leg, plus the SessionStart cursor seed. unit: the_frame_leg_arms_from_a_commune_core_already_ingested, the_frame_poll_authenticates_with_the_sessions_own_sid, a_plain_commune_frame_and_a_peer_message_never_arm (the kind filter - a peer MESSAGE quoting the marker is text, never a trigger), a_truncated_commune_frame_refuses_loudly_instead_of_guessing, the_frame_leg_stands_down_when_an_arm_is_already_pending, a_refused_frame_poll_is_loud, session_start_seeds_the_io_cursor_so_the_first_stop_is_not_the_blind_poll. Mutation-verified six ways including dropping the kind filter and swallowing the refusal. int DEFERRED: the live E2E is a real Bash-written across-commune, written as a turn's final act, firing a real boundary on this node - which is exactly the test v0.37.0 failed. [[requirements]] id = "REQ-HAZARD-COMMUNE-DROP-REARM" title = "The drop detector examines each drop AT MOST ONCE PER IDENTITY and arms at most once: state/commune-seen/.stamp records the LAST EXAMINED identity (:), an unchanged identity is a no-op that does NOT read the file, a changed or missing stamp triggers read-scan-arm-and-restamp, and drop absence clears the stamp. HAZARD INTRODUCED BY REQ-COMMUNE-DROP-DETECT, and it is a shape change rather than a detail: the old trigger was an EVENT (one Write tool call, observed once by construction), the new one is a STATE (a file that persists until core ingests it, >15s and many tool calls later), and every state-shaped trigger re-fires unless something remembers. Without the stamp the detector arms on EVERY hook for the whole ingest window - a second wake-arm self-send, a rewritten clearing latch, and ultimately two post-clear sequences, which BLEND rather than fail loudly (the v0.14.1 boundary-rename scar: never emit two post-clear sequences). The stamp records EXAMINED rather than ARMED deliberately: a commune with no wake marker is the common case, and recording only arms would re-READ a 20-100KB file on every tool call for 15 seconds while never arming. Both halves are load-bearing and both get a test - an arms-exactly-once test and a stat-not-read test asserting the cost model, since a detector that is correct but re-reads is a regression nobody would notice. EXTENDED 2026-09-06 (v0.38.0) TO A SECOND SOURCE: with REQ-COMMUNE-FRAME-BACKSTOP the same drop can be seen TWICE - once on disk before ingest, once as a COMMUNE frame after - so the frame leg stands down whenever a clearing latch is already armed. One drop, one boundary, whichever leg saw it first." required_stages = ["impl", "unit"] # ACTIVATED 2026-09-06 with its sibling. impl: `commune_seen_rel` + the examined-stamp read/restamp/clear in `examine_commune_drop`, `stamp_commune_drop`, and the shortcut's stamp of its own write in `scan_and_dispatch`. unit: one_drop_arms_exactly_once_across_every_leg_and_every_hook, an_unchanged_drop_is_stat_but_never_re_read (the COST half), a_second_different_drop_at_the_same_path_arms_again (the stamp is a memory, not a latch), an_ingested_drop_forgets_its_stamp, the_commune_shortcut_stamps_its_own_write_so_the_detector_does_not_arm_twice. Mutation-verified four ways: removing the stamp guard, stamping ARMED instead of EXAMINED, dropping the shortcut's stamp, and letting the stamp outlive the file each fail their own tests. docs/KNOWN-HAZARDS.md § 9.1. WAS PLANNED with stages held empty until this commit. Needs its docs/KNOWN-HAZARDS.md entry in that SAME change (rule 4: a hazard is not covered until it has a test). [[requirements]] id = "REQ-TAG-PEER-MESSAGING" title = "RETIRED 2026-08-29 by REQ-IO-COMPLIANCE-DECLARED (Release B): this adapter no longer parses or dispatches shortform - spt-core does, off the ingest payloads we report, and per-target outcomes surface through the now-signal DISPATCH_RESULTS category, which core documents as the only channel by design. The evidence this requirement described has been DELETED, not merely left untagged, so the stages are cleared rather than the requirement removed: what it protected is kept here as the reasoning that has to outlive the code. ORIGINAL: An agent messages peers WITHOUT the Bash tool by embedding `@` in its turn output: `@<` opens, the comma-separated target list runs to the first whitespace (no internal spaces), the body runs to the first `@>` (non-greedy, no escaping v1); a bare `@@id` is deliberately inert (prose discussing the syntax must not false-fire). A PreToolUse (mid-turn) + Stop (backstop) scan pulls the endpoint's own finalized digest output newer than a seq cursor (`endpoint digest --after --json`, `entries[].Agent.text` with a `seq`), dispatches `spt send --from ` per valid target (self-target dropped+noted; unknown/offline surfaced via NO_PERCH, never silent-dropped; delivered = SENT/QUEUED/DEFERRED), and fires ONE confirm-back to the sender's OWN perch (`--active-only --ephemeral`, lands mid-turn) so the agent does not re-send via Bash. The seq cursor (state/digest/.seq) is the exactly-once double-send guard across both hooks — only seq-carrying (finalized) entries strictly above the cursor dispatch; a partial never fires." required_stages = [] # RETIRED 2026-08-29 (Release B) - evidence deleted with the local parser. WAS: ["impl", "unit"] # ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 2). impl: src/tag_scan.rs (pure grammar parse_tag_sections + seq-gated plan_dispatch) + hook.rs scan_and_dispatch wired into handle_pre_tool_use (mid-turn) and handle_stop (backstop), send_landed classifier, digest_cursor_rel cursor. unit: tag_scan.rs grammar + plan_dispatch tests + hook.rs scan_and_dispatch wiring tests (multi-target dispatch, unreachable-surfaced, self-drop, cursor advance, no-op-when-current). int DEFERRED (same harness-glue bucket as the other live E2Es — needs the built binary bounced onto a live endpoint): an agent emitting `@` actually delivers to doyle + the confirm-back lands. Built on the published surface only (`endpoint digest --after`, `spt send --from/--active-only/--ephemeral` all verified present in spt 0.29.0) — adapter wiring, not an spt-core gap. [[requirements]] id = "REQ-COMMUNE-OUTPUT-SHORTCUT" title = "An agent writes its commune WITHOUT the Write tool by starting an output with `>>commune<<` (after leading whitespace); the rest of that output is the commune body, written to `.claude/-commune.md` under the session cwd — the same file the /sptc:commune Write path targets, so the daemon's watch ingests+deletes it identically. The existing `!!checkpoint!!` in-body marker is reused for the checkpoint escalation (no new keyword): a commune body carrying it fires the same idle + checkpoint self-send loopback as the Write-path PostToolUse handler. COLLISION RULE (meta-recursion guard): once an output is a commune it is NOT scanned for `@<…@>` — a commune may quote the messaging syntax verbatim without dispatching." required_stages = ["impl", "unit"] # ACTIVATED 2026-07-08 (NEXT-WORKLOAD-PLAN Item 2). impl: tag_scan::commune_body detector + plan_dispatch commune routing (suppresses tag scan) + hook.rs scan_and_dispatch commune leg (write_commune_file to cwd .claude/-commune.md via write_spill; has_checkpoint→checkpoint_payload/checkpoint_wake reuse). unit: tag_scan.rs commune_body tests (start-only, leading-ws, checkpoint-preserved) + plan_dispatch commune-suppresses-tags test + hook.rs scan_commune_shortcut_writes_file_and_escalates_checkpoint. int DEFERRED (needs the built binary on a live endpoint + daemon watch): a `>>commune<<` output actually ingests as the commune. Reuses the proven checkpoint loopback (REQ-DIST-CHECKPOINT-COMMUNE); built on the published surface only. NOTE 2026-07-08 (operator decision): the shortcut is INTENTIONALLY UNDOCUMENTED for agents — the `>>commune<<`-must-be-the-FIRST-thing-in-output constraint fights how agents write (they narrate, THEN act), so the marker is routinely missed and the commune silently doesn't fire (perri hit it 3× in one session). The impl + unit stay (harmless when unused, reversible, works for a correct caller); the teaching was removed from the agent-facing surfaces (adapter/strings/briefs/messaging-perch.md + plugin/sptc/skills/send/SKILL.md), which now point agents at the reliable /sptc:commune + Write path. The `@<…@>` peer shortcut is unaffected and still taught. [[requirements]] id = "REQ-COMMUNE-CONTEXT-TIER-SLICING" title = "The two adapter-authored commune prompts must teach the two-slice context envelope so spt-core routes context per-agent-per-project instead of dumping everything in the live tier. spt-core ingests a commune (and the echo-commune summary) into durable tiers — `` (agents//live-context.md, cross-project identity) and `` (projects///project-context.md, this-project detail), the same tiers `psyche-download` re-emits on resume — and routes by these tags; UNTAGGED text defaults to the live tier, so an unsliced commune pollutes the durable identity with project-specific noise that also leaks across projects (doyle finding 2026-07-08). FIX (prompt-only, both authoring paths): (1) the live-agent commune brief (adapter/strings/briefs/live-ops.md) instructs the agent to wrap project detail in `` and cross-project/role context in ``; (2) the echo-commune summarizer directive (echo_commune.rs compose_prompt) emits the same two slices for the no-signoff auto-summary. Untagged-defaults-to-live is stated in both so partial tagging is safe. The `` sentinel is core-emitted OUTPUT only — never agent-authored." required_stages = ["impl", "unit"] # MINTED + ACTIVATED 2026-07-08 (doyle two-slice finding; folded into v0.17.3). impl: tools/claude-spt/src/echo_commune.rs compose_prompt two-slice directive + adapter/strings/briefs/live-ops.md commune brief two-slice guidance (both authoring paths). unit: echo_commune.rs prompt_teaches_context_tier_slicing (asserts both ``/`` slices + the default-tier rule in the summarizer directive). Prompt-only; the ingest routing itself is spt-core's (published tier contract, CHECKPOINT-COMMUNE-PLAN §STDOUT). No int (behavioral routing is core-side; the adapter's obligation is the prompt text)." === TAIL === [[requirements]] id = "REQ-NOW-SIGNAL-AUTHORED-TEXT-TRACE" title = "A now-signal poll RECORDS the authored text this adapter handed it, under the existing `$SPTC_HOOK_TRACE` opt-in. WHY: spt-core gathers a category's matches out of the poll's AUTHORED TEXT — the `--user-input` and `--agent-output` values COMBINED — tokenized on anything that is not alphanumeric / `-` / `_`, lowercased (so `lia:`, `(lia)`, `\"lia\"` and `LIA` all match, while `foo-lia` is one token and does not). Nothing recorded what we actually passed, so a seen-set entry appearing for a token we handed over is indistinguishable, from outside, from the category inventing it. FIELD 2026-08-28: `lia` entered ENDPOINT_MENTIONS' seen-set at 20:49:24 on a turn whose prompt was an ADR-0007 stub carrying no such token; the operator then asked about lia and the category was correctly SILENT (already seen), which read as the feature failing. doyle's census of core settled that there is exactly ONE writer of that seen-set and no seeding pass, description-fetch or state sync — so a real token matched — and neither his grep nor my reconstruction of the inputs could say which, because the authored text was never captured. This requirement is that capture: the next unexplained entry names its own source instead of costing a peer a grep of their tree. IT IS A MEASUREMENT, NOT A FIX, and deliberately so — the anomaly is unexplained, and an instrument that makes the next occurrence self-describing beats a guess at this one. TRACE-GATED, which is part of the requirement rather than an implementation choice: the `spt` seam does not log argv, so user prompts have NEVER been written to `hook-trace.log`, and a default-on capture would start persisting every prompt to a rolling on-disk file. Same discipline as the Stop-leg trace and REQ-TAG-SEND-STAMP-PROBE (which records a classification and never the session id value) — the operator opts in for a diagnosis and pays nothing otherwise. The line NAMES THE CHANNELS THE CALL ACTUALLY PASSED, per channel, so the record can never be read as the whole authored text when it is only part of it. AMENDED 2026-08-29 (REQ-NOW-SIGNAL-PRETOOL-AGENT-OUTPUT): the original line hardcoded `agent-output NOT PASSED by this adapter`, which was true of every call this adapter made and became FALSE the moment the PreToolUse leg started feeding that channel — a capture that lies about the input it exists to explain is worse than no capture, so the not-passed wording is retired and each leg names what it passed. The asymmetry that clause recorded is the reason the requirement exists: an earlier control found that a name written in the agent's own output did not enter the seen-set, which proved a property of THIS ADAPTER — that it never passed that channel — and nothing whatever about the contract, which reads both. The contract side has since been measured directly (two fresh session ids, one variable, agent-output alone driving ENDPOINT_MENTIONS)." required_stages = ["impl", "unit"] # MINTED + ACTIVATED 2026-08-28 with the implementing commit. impl: tools/claude-spt/src/hook.rs — the $SPTC_HOOK_TRACE-gated capture at the now-signal call site. unit: hook.rs — opted in, the exact authored text and the not-passed-channel note are recorded; opted out, neither the marker nor the prompt text reaches the log at all (the default-off privacy arm, asserted rather than assumed). No "int": the gathering and its tokenizer are core's. [[requirements]] id = "REQ-NOW-SIGNAL-PRETOOL-AGENT-OUTPUT" title = "claude-spt injects the now-signal funnel at PreToolUse as well as UserPromptSubmit — the SECOND HALF of the published cadence (`harness-contract/api`: 'Inject it on every UserPromptSubmit- and PreToolUse-equivalent; that cadence is what it is built for') — and that leg is what finally feeds `--agent-output`, a channel this adapter had never passed on any call. WHY IT IS A GAP AND NOT A NICETY: spt-core gathers ENDPOINT_MENTIONS, MONICS and HINTS out of the poll's authored text, which is the `--user-input` and `--agent-output` values COMBINED. Feeding only the first means every agent_output-triggered monic and hint in the fleet is INERT for Claude Code sessions — not degraded, not lagging: it cannot fire at all, in any session, with or without `[io]` compliance. REQ-NOW-SIGNAL-INJECT deferred this leg on the reasoning that 'until compliance is declared DISPATCH_RESULTS has nothing to report, so a mid-turn window buys little today'; that weighed the leg on DISPATCH_RESULTS alone, and with claude-spt-bs#22 blocked on spt-releases#238 with no date, the un-weighed half became an open-ended outage. PreToolUse is the ONLY injection point where the agent has authored text for the CURRENT turn, which is why the capability lives here and nowhere else. MEASURED BEFORE BUILDING (this node, 2026-08-29), because the deferral was a cost argument and a cost argument deserves a number: ~75 ms per poll (83/73/73/77 ms over four runs), one process spawn on a hook that already spawns two; and the CONTEXT cost after the first poll is ZERO BYTES, observed, not read — run 0 printed 432 B and runs 1-3 printed nothing at all, which is what delta-only means in practice and what makes a per-tool-call cadence affordable. The channel itself was proven the same way, with the prediction stated before the reading: two fresh session ids differing in one variable, agent-output carrying an endpoint token printed ``, agent-output carrying none printed no such block. That control is the one the previous session could not take — its predecessor showed only that THIS ADAPTER never passes the channel, a property of us and not of the contract, which is precisely the vacuous-control trap REQ-NOW-SIGNAL-AUTHORED-TEXT-TRACE was minted to record. SHAPE, and each clause is a hazard rather than a preference: (a) the authored text is the batch `scan_and_dispatch` ALREADY consumed at its transcript byte cursor — one read, one cursor, so the poll and the dispatch can never disagree about what is new, and no second tail read double-counts text across polls; (b) COMMUNE BODIES ARE EXCLUDED — a commune is a whole context dump full of endpoint names, and handing one over would mark every one of them seen in a single poll, after which a genuine mention is SILENT, which is indistinguishable from the feature being broken; (c) skipped under the across-clear quiet latch, like the poll and the park drain, because the session is about to be rebuilt and a fresh sid is entitled to the picture once; (d) subagents never reach it — `handle_pre_tool_use` returns before any of this for a subagent (REQ-HAZARD-SUBAGENT-DRAIN-STEAL), so a subagent's authored text cannot enter the parent's poll; (e) the argv cap is `now_signal_arg` unchanged, safe here for the same reason it is safe on `--user-input` (this text is SCANNED, so a cut costs at most a tail match) and still never safe for the io state payload; (f) placed AFTER the poll and the park drain and before emit — the UPS ordering, so a signal can never delay a message. NOT a compliance change: the funnel's authored text is scanned for names and keywords, never parsed for shortform (that parse reads the `api state` payloads and only under `[io]` compliance), so this adds no dispatch path and #22 stays exactly as held." required_stages = ["impl", "unit"] # MINTED + ACTIVATED 2026-08-29 with the implementing commit. impl: tools/claude-spt/src/hook.rs — the now-signal arm in handle_pre_tool_use + the authored-text carry out of scan_and_dispatch. unit: hook.rs — a perched mid-turn tool call polls the funnel with its own authored text on --agent-output and never with --user-input; a commune-bearing text is NOT handed to the channel; the quiet latch skips the poll entirely; an empty answer injects nothing; a subagent call makes no poll at all. No "int": the categories, their tokenizer and their delta rules are spt-core's. [[requirements]] id = "REQ-IO-MIDTURN-SPAN-REPORT" title = "claude-spt REPORTS the agent's MID-TURN output as it lands, via spt-core's `spt api state busy --payload-stdin --mid` (spt-releases#238, shipped in core v0.66.0), so an AGENT_OUTPUT event exists for text written BEFORE the turn's closing message. Release A of claude-spt-bs#22. WHY: REQ-IO-AGENT-OUTPUT-PAYLOAD publishes only the turn's CLOSING output, and `turn_closing_output` returns the LAST non-empty assistant text — so every non-final assistant message in a tool-bearing turn was never published at all. Measured on this node over a ~25h window (hook-trace.log, 726 lines, 147 KB against a 512 KB roll so the window is complete): 32 tag sends, 28 turn-end and 4 MID-TURN, i.e. 12.5% of dispatches sat in text no io consumer could see. That number is the whole reason claude-spt-bs#22 was HELD on 2026-08-28 rather than shipped with a known silent loss; this requirement recovers it. THE CONTRACT, read on the node-local book after the 0.66.0 update (`shells/frames` + `harness-contract/manifest` + `spt api state --help`, all three agreeing): `--mid` marks the payload a mid-turn SPAN rather than the turn's close, rides the BUSY arm because the agent is still working, and is still an AGENT_OUTPUT event; it surfaces as mid=\"1\" on the frame and \"mid\": true in `api io-events`, PRESENT-ONLY, so an event without it is a turn's close exactly as every AGENT_OUTPUT was before the flag existed. `--mid` at idle is refused by name (STATE_MID_ON_IDLE) and `--mid` with no payload is refused (STATE_MID_NO_PAYLOAD): both are contradictions rather than events. THE SPAN SOURCE IS THE BATCH ALREADY CONSUMED, not a new read: `ScanOutcome.authored` — the same cursor-guarded batch that feeds the PreToolUse now-signal poll (REQ-NOW-SIGNAL-PRETOOL-AGENT-OUTPUT). One read, one cursor. A second independent tail read is exactly how two consumers come to disagree about what is new, and here disagreement is not a stale picture but a DOUBLE PUBLISH. Consequences that are hazards rather than preferences: commune bodies stay excluded (that exclusion now also keeps a whole context dump out of the io funnel); subagents never reach the leg because `handle_pre_tool_use` returns early for them (REQ-HAZARD-SUBAGENT-DRAIN-STEAL), so a subagent's output can never be published as the parent's span; and the across-clear quiet latch skips it, like the poll and the park drain. GRANULARITY IS THE BATCH, not the message: one `--mid` call per PreToolUse rather than one per assistant message — the contract requires spans to be disjoint and exactly-once, never message-aligned, and a batch is one process spawn per tool call instead of N. NO ADAPTER-SIDE TRUNCATION, restated because this is a second payload seam and the reasoning is easy to lose: the 16 KB cap bounds the emitted FRAME, not the parse — core parses the full text reported and only then bounds the body, which is what truncated=\"1\" and the digest `seq` pointer exist to announce. Cutting here would deliver the same bytes with the flag ABSENT: a silent loss wearing the shape of a complete payload. STRICT, WITH A PAYLOAD-FREE FALLBACK, the seam both existing payload legs use: `env.spt` is exit-code-blind, and a refused busy mark reported through it is invisible while inbound stops spooling — the stuck-ACTIVE class arriving by a new road. So the call reads its exit code and re-marks busy WITHOUT the payload on ANY refusal, which cannot double-publish because a payload-free `state` call emits nothing. NOT a compliance change: `[io]` stays absent this release, so core parses none of this and no dispatch path is added. That separation is deliberate and is the release boundary itself — see REQ-HAZARD-IO-SPAN-OVERLAP." required_stages = ["impl", "unit"] # MINTED + ACTIVATED 2026-08-29 with the implementing commit (claude-spt-bs#22 Release A). impl: tools/claude-spt/src/hook.rs — the span report in handle_pre_tool_use, fed from ScanOutcome.authored. unit: hook.rs — a perched tool call reports its batch with `state busy --payload-stdin --mid` and the span text on stdin; a refused span still marks the endpoint busy and logs loudly; an empty batch reports NO span (STATE_MID_NO_PAYLOAD is a refusal, so the call must not be made at all); a commune body is never published as a span; a subagent call publishes nothing; the quiet latch skips it. No "int": the emission is core-side, and the end-to-end proof is this release's field verification through `spt api io-events`. [[requirements]] id = "REQ-HAZARD-IO-SPAN-OVERLAP" title = "The spans claude-spt reports mid-turn and the payload it reports at the turn's close are DISJOINT: every assistant text is published EXACTLY ONCE per turn. spt-core's rule is explicit and closed — 'Report every span EXACTLY ONCE across the turn. spt-core does not deduplicate and never will... the spans you report mid-turn and the remainder you report at idle must be disjoint. Overlap is not refused; it is delivered twice.' Core declines the dedup deliberately, because knowing two payloads are the same span means modelling how a harness assembles a turn, which is the adapter's to know. THE DEFECT THIS GUARDS WAS ALREADY IN THE TREE, latent and invisible: `turn_closing_output` reads the transcript from OFFSET 0 and returns the last non-empty assistant text, while `scan_and_dispatch` runs FIRST in `handle_stop` (settle=true) and has already advanced the byte cursor past that same text. Two readers, one of them cursor-blind. Today that costs nothing — the closing payload merely restates text our own parser just handled, and with `[io]` absent core parses none of it — but it is reachable two ways the moment spans ship: (a) the agent writes text A, calls a tool, and ends the turn with no further text, so A is published as a span and again as the closer; (b) a turn that produces no assistant text at all walks BACKWARDS into the previous turn's final message and republishes it. WHY IT IS A HAZARD AND NOT A BUG: the cost of an overlap is not constant across the migration. Before `[io] compliance = true` it is a duplicated record, visible in `spt api io-events` and costing nobody anything. After the flip the same overlap is a duplicate MESSAGE DELIVERED TO A PEER — core parses the payload for shortform, so a re-reported `@<…@>` dispatches twice, and the second delivery is detected by the recipient rather than by us. That asymmetry is why Release A ships spans with compliance OFF: it puts the first field test of disjointness on the near side of the flip that makes a mistake externally visible. THE FIX IS STRUCTURAL, NOT A CHECK: stop having two readers. The closing payload comes from `ScanOutcome.authored` — the same cursor-guarded batch the Stop scan already consumed — so disjointness holds BY CONSTRUCTION, from the same byte cursor that has prevented double tag-dispatch across PreToolUse and Stop since v0.17.x, rather than from two call sites agreeing to stay in step. A dedup check comparing span text against closing text would be the wrong shape: it would re-derive at every Stop a fact the cursor already knows, and would silently pass the day the two texts differ by a byte. SEMANTIC CHANGE, STATED RATHER THAN SLIPPED IN: the closing payload becomes 'every assistant text not already reported as a span' instead of 'the last assistant message'. On a turn with no tool calls the two are identical; on a tool-bearing turn the new form is the correct one, and the old form's silent drop of the non-final messages IS the 12.5% loss REQ-IO-MIDTURN-SPAN-REPORT exists to recover. It also discharges an implicit dependency rather than preserving it: the Stop settle loop (REQ-HAZARD-TAG-STOP-SCAN-EARLY) exists to wait out CC writing the turn's final message, and `turn_closing_output` was relying on it from a separate call site that never named it." required_stages = ["doc", "impl", "unit"] # MINTED + ACTIVATED 2026-08-29 with the implementing commit (claude-spt-bs#22 Release A). doc: docs/KNOWN-HAZARDS.md. impl: tools/claude-spt/src/hook.rs — the Stop payload reads ScanOutcome.authored; turn_closing_output is retired. unit: hook.rs — a tool-bearing turn publishes each assistant text exactly once across the span calls and the closing payload (the defining test); a turn whose assistant text was entirely consumed as spans reports idle with NO payload rather than an empty one; a turn that produces no new assistant text never republishes the previous turn's closer. Mutation-verified: re-pointing the closing payload at a fresh offset-0 read must make the exactly-once test FAIL, with the mutation asserted to have actually applied before the run is believed. [[requirements]] id = "REQ-IO-COMPLIANCE-DECLARED" title = "claude-spt DECLARES `[io] compliance = true` and DELETES its own `@<...@>` parser in the SAME release, so spt-core becomes the single parser of this adapter's ingest. The published ordering rule is the whole design: 'an adapter that ships its own tag parser today keeps being the only parser until its own release deletes that parser and declares compliance in the same change - so no version exists in which both parse the same text and send it twice. Declare compliance in the release that removes your local parser, not before.' WHAT CORE NOW PARSES: the payloads this adapter reports at its ingest edges - USER_INPUT on `state busy` (REQ-IO-USER-INPUT-PAYLOAD), the turn's closing AGENT_OUTPUT on `state idle` (REQ-IO-AGENT-OUTPUT-PAYLOAD), and the MID-TURN AGENT_OUTPUT spans shipped in Release A (REQ-IO-MIDTURN-SPAN-REPORT). It never parses a RECEIVED message body: 'a peer can write a tag at you all day; it is text. Nothing you receive can make you send.' RELEASE A IS THE PRECONDITION, not a convenience. Before spans existed a tag written mid-turn reached no ingest edge at all, so declaring compliance would have silently dropped 12.5% of dispatches (measured); and before the disjointness fix (REQ-HAZARD-IO-SPAN-OVERLAP) an overlapping payload would have re-dispatched a tag to targets the author addressed once. Both were shipped and FIELD-VERIFIED through `spt api io-events` (2 spans, 3 closers, 0 duplicate payloads) before this flip, which is exactly what the two-release split bought. WHAT IS DELETED, and it must be the whole parser rather than a convenient half: `tag_scan`'s peer-dispatch grammar (`TagSection`, `parse_tag_sections`, `push_section`, `code_ranges`, `in_code`, `Dispatch.sends`) and, in `hook.rs`, the `spt send` fan-out, the outcome tallies and their classifications, the stamp probe, the outcome ledger, the QUEUED tripwire, and the whole shortform-confirm surface (`ScanConfirm`, `park_turn_end_confirm`, `tag_confirm_rel`, `parked_confirm_block`, and the UserPromptSubmit / PreToolUse sites that surfaced a parked confirm). A SPLIT PARSER IS FORBIDDEN - keeping ours for any case reopens the double-send window the gate exists to close, and a disabled parser is still a parser one edit away from firing. WHAT SURVIVES, deliberately: the commune shortcut (`>>commune<<`, REQ-COMMUNE-OUTPUT-SHORTCUT) is ours and core knows nothing of it; the transcript primitives `assistant_texts_from_jsonl` and `complete_line_prefix`, which the io payload legs depend on; the byte cursor and its first-run narrowing (REQ-HAZARD-TAG-FIRST-RUN-DROP), whose consumer moved from the dispatch to the payload; and the Stop settle (REQ-HAZARD-TAG-STOP-SCAN-EARLY), which Release A repointed at the payload read. `shortform = false` IS NOT DECLARED: it is the exotic-harness opt-out, and declaring it would turn core's reader off while ours is already gone, leaving NO parser rather than one. OUTCOMES MOVE TO DISPATCH_RESULTS, core's now-signal category, which is documented as 'the only channel, by design - a dispatch does not echo into your output, does not reply to you, and does not print a confirmation line, because a second channel is how an author ends up trusting whichever one they happened to notice.' Our own confirm surface was richer, and it is retired rather than kept beside it for exactly that stated reason." required_stages = ["impl", "unit"] # MINTED + ACTIVATED 2026-08-29 with the implementing commit (Release B). impl: adapter/claude-spt.toml `[io] compliance = true`, plus the deletions in tools/claude-spt/src/tag_scan.rs and tools/claude-spt/src/hook.rs. unit: hook.rs + tag_scan.rs - a `@<...@>` in agent output produces NO send call from this adapter at all (asserted rather than assumed: a surviving dispatch path is silent here and visible only as a peer receiving everything twice) while the tag still reaches core verbatim inside the span; a commune body still routes to the commune file; the byte cursor and its first-run narrowing still hold. No "int": the parse and the dispatch are core-side once declared. [[requirements]] id = "REQ-HAZARD-ACROSS-CLEAR-DISPATCH-DROP" title = "A turn inside the ACROSS-CLEAR QUIET WINDOW still reports its authored text, so a shortform tag written in that turn is still dispatched. THE INVARIANT IS OLDER THAN THIS RELEASE and was stated at the very call site about to break it: 'The dispatch above already ran - outbound is never suppressed.' Under our own parser that held for free, because `scan_and_dispatch` dispatched BEFORE `handle_stop` reached the quiet-window return. Under `[io] compliance` the dispatch is core's, and core only sees what we REPORT - so the quiet window, which returns before the idle payload report and skips the PreToolUse span leg, would silently stop dispatching every tag in the turn that arms a commune-across. That is the worst turn in the system to lose one on: it is the turn an agent writes its handoff in, the session is about to be rebuilt, and nothing downstream would record that the message was never sent. THE GENERAL RULE THIS INSTANCE TEACHES: when a capability moves from the adapter to core, every path that SKIPS REPORTING silently becomes a path that SKIPS THE CAPABILITY. An early return that was previously about one concern inherits every concern that now rides the report, and the compiler cannot find them because nothing about them changed. Re-walk the early returns whenever you hand something over. THE FIX RIDES THE BUSY ARM, which is what makes it compatible with the window rather than a hole in it. The window exists to keep the endpoint BUSY across the `/clear` boundary so inbound spools for the post-clear session, and marking IDLE is the thing it must not do; `state busy --payload-stdin --mid` marks busy (idempotent - it already is) and publishes the batch as a mid-turn AGENT_OUTPUT span, which core parses. So the text reaches the parser, the endpoint stays ACTIVE, and the window keeps doing its job. The span is the same cursor-guarded batch every other leg reads, so exactly-once still holds (REQ-HAZARD-IO-SPAN-OVERLAP). ACCEPTED AND NAMED: a BARE (odd, unpaired) `;;` seal marker in that turn is refused as SEAL_BARE_MIDTURN, because 'through the end of your output' names text core has not been given yet and a short seal looks exactly like a correct one. Seal PAIRS mint normally, and a bare marker is the one construction an across-clear turn loses. Recorded rather than worked around: closing the pair is the author's fix, and a short seal that looked correct would be worse than a named refusal." required_stages = ["doc", "impl", "unit"] # MINTED + ACTIVATED 2026-08-29 with the implementing commit (Release B). doc: docs/KNOWN-HAZARDS.md. impl: tools/claude-spt/src/hook.rs - the quiet-window arm of handle_stop reports the batch as a `--mid` span before returning. unit: hook.rs - a Stop inside the quiet window publishes its authored text as a span and NEVER marks idle; an empty batch in the window reports nothing at all. Mutation-verified: removing the span call, and adding an idle mark beside it, each fail the intended test.