import io

p = r'C:\Users\decid\Documents\projects\spt-claude-code\traceable-reqs.toml'
s = io.open(p, encoding='utf-8').read()

Q = chr(34)
BS = chr(92)
ESCQ = BS + Q  # an escaped double-quote for inside a TOML basic string

span_title = (
 "claude-spt REPORTS the agent's MID-TURN output as it lands, via spt-core's "
 "`spt api state busy <id> --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=" + ESCQ + "1" + ESCQ + " on the frame and " + ESCQ + "mid" + ESCQ + ": 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=" + ESCQ + "1" + ESCQ + " 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."
)

span_stages = (
 '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`."
)

hazard_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."
)

hazard_stages = (
 '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.'
)

NL = chr(10)
block = (
    NL + "[[requirements]]" + NL
    + 'id = "REQ-IO-MIDTURN-SPAN-REPORT"' + NL
    + 'title = "' + span_title + '"' + NL
    + span_stages + NL
    + NL + "[[requirements]]" + NL
    + 'id = "REQ-HAZARD-IO-SPAN-OVERLAP"' + NL
    + 'title = "' + hazard_title + '"' + NL
    + hazard_stages + NL
)

assert 'REQ-IO-MIDTURN-SPAN-REPORT' not in s, 'already minted'
assert 'REQ-HAZARD-IO-SPAN-OVERLAP' not in s, 'already minted'
# No bare double-quote may appear unescaped inside a TOML basic string.
for t in (span_title, hazard_title):
    i = 0
    while i < len(t):
        if t[i] == Q and (i == 0 or t[i-1] != BS):
            raise AssertionError('unescaped quote at %d: %r' % (i, t[max(0,i-40):i+40]))
        i += 1

s = s.rstrip(NL) + NL + block
io.open(p, 'w', encoding='utf-8', newline=NL).write(s)
print('appended; requirements now:', s.count('[[requirements]]'))
