import io

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

# Block-aware, never a substring search across the file: these titles cite each other's ids
# constantly, so the first match of an id is usually a CITATION, not the definition.
blocks = s.split("[[requirements]]")
hits = [i for i, b in enumerate(blocks) if b.lstrip().startswith('id = "REQ-DIST-CHECKPOINT-COMMUNE"')]
assert len(hits) == 1, hits
i = hits[0]
b = blocks[i]

old_tail = 'INVARIANT: the commune is authored INLINE pre-clear (ADR-0004, Shape 1), never via a post-clear resume-Self refresh."'
assert b.count(old_tail) == 1, b.count(old_tail)
amend = (
    old_tail[:-1]
    + " AMENDED 2026-09-06 (COMMUNE-DROP-DETECT-PLAN.md, REQ-COMMUNE-DROP-DETECT): the DETECTION half"
    " of this requirement is SUPERSEDED. The trigger is no longer the Write tool_input.content - it is"
    " the DROP FILE itself, examined at PostToolUse/PreToolUse/Stop, because an instrument-keyed rule"
    " gave no boundary at all to an agent writing its commune with Bash. Everything else stands"
    " unchanged: the marker grammar (one = default wake, a pair brackets a custom directive), the"
    " idle/latch mark, the reserved json-payload self-send, the translation-binary loopback, and the"
    ' split clear-then-wake ordering. The PostToolUse Write matcher itself is KEPT as the fastest leg."'
)
blocks[i] = b.replace(old_tail, amend)
out = "[[requirements]]".join(blocks)

new_reqs = '''[[requirements]]
id = "REQ-COMMUNE-DROP-DETECT"
title = "The across-commune boundary trigger is the DROP FILE, never the writing instrument: at every hook carrying a cwd (PostToolUse, PreToolUse, Stop) the adapter asks whether <cwd>/<[session].commune_dir>/<id>-commune.md EXISTS and carries the wake marker, and arms the wake/clear macro if it does - the tool name and tool_input are not read. ROOT: is_commune_write() was `tool == Write && file_path.ends_with(<id>-commune.md)`, an INSTRUMENT-keyed rule, so an agent writing its commune with Bash (heredoc, cat >, python) or Edit got NO boundary at all - silently, at both ends, and increasingly often as agents moved to Bash. spt-core's own model is the opposite and always was: a commune is a FILE-DROP, any harness that can write a file can commune (harness-contract/api.md), with [session].commune_dir the declared seam - the drop is the contract, the tool is not. THREE LEGS, NO NEW REGISTRATION: PostToolUse (matcher stays Write - today's latency for the common instrument), PreToolUse (mid-turn, catches every other instrument), Stop (backstop for a drop written by the turn's final tool call) - the same mid-turn-plus-backstop shape the commune-tag path already uses. Widening PostToolUse to `*` was REJECTED: the matcher lives in the plugin's static hooks.json (a republish), and it would add a second hook spawn to every tool call on top of the PreToolUse one. REJECTED ALTERNATIVE, recorded because the outcome hides the reasoning: the io funnel's COMMUNE kind is the RIGHT predicate and api.md names this exact construct (wake-marker-class constructs over commune and IO events) as its motivating case, but it fires only when INGEST COMPLETES - >15s later, operator-measured - by which time no hook is running; it is also authenticated (--session-id or a capability --token, neither of which a between-turns process holds) and its payload is capped at the 16KB class with truncated/digest_seq, which our communes routinely exceed. A [service] ResidentService is the only process alive between turns, but manifest.schema.json $defs/Service states it has no perch, no identity, and no address, so it cannot make the authenticated call - filed as spt-bs-releases#275 and priced NOT BLOCKING because of this requirement. ACCEPTED RISK, stated so it can be falsified rather than discovered: this design holds only while core's ingest is SLOWER than a hook cycle, which is what guarantees the drop is still on disk when the next hook looks; sub-second ingest would let a Bash-written drop vanish unseen. That is deliberately NOT a hazard requirement - it is a property of core's timing, not of our code, so it cannot be unit-tested and lives as a call-site comment naming the funnel/service path as its fallback."
required_stages = []  # PLANNED 2026-09-06 (COMMUNE-DROP-DETECT-PLAN.md) - ACTIVATE to ["impl", "unit"] at the FIRST IMPL COMMIT, not before: activating ahead of evidence turns the gate RED for every commit until the code lands, which is how a gate stops being read (paid for once already, v0.31.0). doc DEFERRED to task 7 - the agent-facing brief in adapter/strings still steers agents to /sptc:commune + Write and must stop implying the instrument matters. int DEFERRED - the live E2E is a real Bash-written across-commune firing a real boundary on a live endpoint, which needs the built binary bounced onto one.

[[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/<eid>.stamp records the LAST EXAMINED identity (<mtime_ms>:<len>), 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."
required_stages = []  # PLANNED 2026-09-06 - ACTIVATE to ["impl", "unit"] with its sibling at the first impl commit. Needs its docs/KNOWN-HAZARDS.md entry in that SAME change (rule 4: a hazard is not covered until it has a test).

'''

idx = out.index('id = "REQ-DIST-CHECKPOINT-COMMUNE"')
nxt = out.index("[[requirements]]", idx)
out = out[:nxt] + new_reqs + out[nxt:]

io.open(p, "w", encoding="utf-8", newline="\n").write(out)
print("wrote", len(out))
