import io
import re

# ---------------------------------------------------------------- hazard doc: the truncation limit
hp = r"C:\Users\decid\Documents\projects\spt-claude-code\docs\KNOWN-HAZARDS.md"
h = io.open(hp, encoding="utf-8").read()
assert "9.2 A boundary trigger" not in h
h = h.rstrip("\n") + """

<!-- [doc->REQ-COMMUNE-FRAME-BACKSTOP] -->
### 9.2 A detector that reads the disk loses a race it was never told it was in

- **Failure shape:** § 9.1's detector stats the commune drop at each hook. That is correct only
  while the drop is still *there* — and spt-core's watcher ingests and **deletes** it. The design
  was built on an ingest time of ">15 seconds"; measured on the field node it is **2.8s / 3.8s /
  5.8s**. A hook cycle is routinely longer than that, because the gap between hooks includes the
  agent's own thinking time. So the commune written as a turn's *final act* — the handoff, the exact
  case the feature exists for — is typically gone before that turn's `Stop` runs, and every
  file-stat leg sees nothing at all.
- **How it was found:** by field-testing the release. Two real drops (`cp`, then `>`) fired no
  boundary, and `state/commune-seen/` stayed empty, proving no leg had even *examined* them. The
  binary was correct the whole time — driven with a synthetic payload it detects, logs and arms
  exactly as designed. The logic worked; the window it needed did not exist.
- **The general rule, which outlives this instance:** *naming* a load-bearing assumption is not
  *testing* it. The requirement said, in as many words, "this holds only while ingest is slower than
  a hook cycle" — phrased to be falsifiable, and never falsified. The cheapest measurement available
  decided the whole design, and it was taken after the release instead of before it.
- **Invariant (`REQ-COMMUNE-FRAME-BACKSTOP`):** once ingested, the `COMMUNE` io frame is the only
  surviving evidence, and `Stop` reads it — `api io-events <id> --session-id <sid>`, authenticated
  by the sid every hook already carries. The disk leg wins before ingest, the frame leg after; they
  are complementary, not redundant, and between them the window has no gap.
- **Never clear on a guess:** a `truncated` frame is refused **by name** rather than armed or
  silently skipped. The payload caps at the 16KB class and the marker conventionally sits at the end
  of a commune — precisely what a cap cuts — so arming would reset a session that never asked for
  it. These frames carry no `digest_seq` to follow (28 observed), so there is no adapter-side
  recovery; the gap is filed upstream and the refusal names the manual recovery.
- **Lane:** adapter-side. The timing is core's; building on an unmeasured number was ours.
"""
io.open(hp, "w", encoding="utf-8", newline="\n").write(h + "\n")

# ---------------------------------------------------------------- manifest: version + history
mp = r"C:\Users\decid\Documents\projects\spt-claude-code\adapter\claude-spt.toml"
m = io.open(mp, encoding="utf-8").read()
assert m.count('version = "0.37.0"') == 1
history = """# 0.38.0 (2026-09-06): THE FRAME BACKSTOP — v0.37.0 was field-tested and did not fire. Its detector
# stats the drop file, which is correct only while the drop is still there; core's watcher ingests
# and DELETES it, MEASURED at 2.8s / 3.8s / 5.8s on the field node, not the >15s the design assumed.
# A hook cycle is routinely longer than that (the gap between hooks includes the agent's own
# thinking time), so a commune written as a turn's FINAL act — the handoff case the feature exists
# for — is gone before that turn's Stop runs. Two real field drops armed nothing and were never even
# examined. What survives ingest is the COMMUNE io frame, carrying the drop's bytes verbatim, and
# `api io-events <id> --session-id <sid>` is authenticated by the sid every hook already holds. So
# Stop now polls the funnel after the disk leg: disk wins before ingest, frame wins after, and the
# two are complementary rather than redundant. A `truncated` frame is REFUSED BY NAME (the 16KB cap
# cuts exactly where the marker sits, and these frames carry no digest_seq to follow) — never clear
# a session on a guess. The io cursor is seeded at SessionStart because a session's first poll
# returns nothing by design, and a blind first Stop would swallow that session's first commune.
# REQ-COMMUNE-FRAME-BACKSTOP, KNOWN-HAZARDS 9.2, claude-spt-bs#26. Floor unchanged 0.66.0
# (io-events landed well below it); skeleton unchanged 0.1.18.
version = "0.38.0\""""
io.open(mp, "w", encoding="utf-8", newline="\n").write(m.replace('version = "0.37.0"', history, 1))

# ---------------------------------------------------------------- CHANGELOG (UX only)
cp = r"C:\Users\decid\Documents\projects\spt-claude-code\CHANGELOG.md"
c = io.open(cp, encoding="utf-8").read()
anchor = "## [0.37.0] - 2026-09-06"
assert c.count(anchor) == 1
entry = """## [0.38.0] - 2026-09-06

> Requires spt-core **v0.66.0 or newer** (unchanged). Update with `spt adapter update claude-spt`. Plugin skeleton **0.1.18** (unchanged).

### Fixed
- **A commune-across written as the last thing an agent does now resets its context reliably.** v0.37.0 made the reset independent of which tool wrote the commune, but it still had to notice the file before spacetime had finished filing it away — a few seconds — so a commune written as an agent's final act usually missed its window and the reset silently did not happen. The adapter now also asks spacetime what it filed, which is what remains after the file is gone, so the handoff case works whether or not the file is still there.

### Known limitation
- A very large commune (past roughly 16KB) that spacetime has already filed cannot be checked for the wake marker, so no reset is fired and the adapter says so in its log rather than guessing. Re-issue the commune or reset manually. Communes written mid-work are unaffected — the file is still on disk and is read in full.

"""
c = c.replace(anchor, entry + anchor, 1)
io.open(cp, "w", encoding="utf-8", newline="\n").write(c)
heads = re.findall(r"^## \[(\d+\.\d+\.\d+)\]", c, re.M)
assert heads[:3] == ["0.38.0", "0.37.0", "0.36.0"], heads[:5]
print("ok, headings:", heads[:4])
