# IO-parser compliance — JIT plan (claude-spt-bs#22)

Ship **one** release that declares `[io] compliance = true` **and** deletes our local `@<…@>`
parser, per the published ordering rule: *"Declare compliance in the release that removes your local
parser, not before."* No version may exist in which both parsers run.

Grounded on the node-local docs (`http://localhost:5474`) read this session:
`harness-contract/manifest` (`[io]`, `[io.now_signal]`), `harness-contract/api` (`state
--payload-stdin`, `now-signal`), `shells/frames` (shortform grammar, suppression, DISPATCH_RESULTS,
the kind vocabulary and the 16 KB cap).

## Scope-defining findings (established, do not re-derive)

1. **Shortform is parsed off the STATE PAYLOAD, not off now-signal.** `harness-contract/api`:
   "*Parsing this payload for shortform happens only when the manifest declares `[io]` compliance*",
   directly under `state busy --payload-stdin` (USER_INPUT) / `state idle` (AGENT_OUTPUT).
   `now-signal --user-input` is documented as feeding ENDPOINT_MENTIONS / HINTS / MONICS only.
   ⇒ **v0.32.0 ships the idle payload only, so an operator-typed `@<…@>` would stop dispatching
   the moment we declare compliance.** This release MUST also ship the busy payload. This was the
   pre-flagged blocker; it is real.
2. **Nothing calls `spt api now-signal` today.** DISPATCH_RESULTS is *"the only channel"* that
   reports dispatch and seal outcomes — so deleting our confirm machinery without wiring now-signal
   leaves the author with no outcome channel at all. The migration is `api hint` → `api now-signal`
   (hint is a documented thin alias over the HINTS category; injecting both injects it twice).
3. **The vendored manifest schema is stale** — no `io` (and no `service`). Refresh it from
   `http://localhost:5474/manifest.schema.json`. That node-local URL is the CANONICAL one, not a
   fallback: `https://sabermage.github.io/spt-releases/manifest.schema.json` 404s permanently, as
   does the whole Pages host, because ADR-0036 ("The Forkening", 2026-07-14) killed public Pages and
   privated the releases repo — the schema's own `$id` already names localhost:5474, and the signed
   docs bundle ships with every release so the schema always matches the installed core. NOT an
   outage; I first reported it as one and doyle corrected the record. The real defect the report
   surfaced is that `docs-site/reference/schema.md` still calls the dead Pages URL "the stable URL"
   (hand-written prose the drift gate cannot see); core is amending that and two sibling pages.
4. **`tag_scan.rs` does NOT get deleted wholesale.** It also owns the commune-output shortcut
   (`>>commune<<`) and two transcript primitives (`assistant_texts_from_jsonl`,
   `complete_line_prefix`) that `turn_closing_output` — the AGENT_OUTPUT payload read — depends on.
   Delete the peer-dispatch half only: `TagSection`, `code_ranges`, `in_code`, `parse_tag_sections`,
   `push_section`, and `Dispatch.sends`.
5. **TRAP — the settle is load-bearing and about to be deleted by accident.** `handle_stop` calls
   `scan_and_dispatch(settle = true)` *before* `turn_closing_output`. That settle loop is what waits
   out CC writing the turn's final assistant message (MEASURED 2026-07-26: the message lands
   *after* Stop fires — `REQ-HAZARD-TAG-STOP-SCAN-EARLY`). Remove the scan and the payload read runs
   early, so the AGENT_OUTPUT we publish is the *second-to-last* message — and under compliance that
   silently drops every turn-final tag, which is exactly the regression this release exists to avoid.
   **The settle must move into the payload read.**

## RULING — compliance is HELD this release (operator, 2026-08-28)

doyle answered both questions from the published contract:

- **Mid-turn tags are an accepted loss under compliance.** Core parses shortform only out of the
  ingest edges the adapter reports, and AGENT_OUTPUT is the turn's *closing* output — so a tag in a
  non-final assistant message never reaches core's parser until `spt-releases#238` (mid-turn IO
  frames) lands. **A split parser is forbidden**: the declaration states our parsers are gone, full
  stop; keeping ours for mid-turn only reopens the double-dispatch window the gate exists to close.
  The two legal options are declare-and-lose-mid-turn, or hold the declaration and keep the whole
  local parser. doyle recommends holding.
- **Parse happens over the FULL payload we pass** — the 16 KB cap bounds the emitted frame body
  (that is what `truncated="1"` and the digest pointer are for), never the parse input. A tag in the
  tail of a >16 KB payload still dispatches. Ruling of record; the docs amendment is queued behind
  core's v0.65.0 tag window.

**Measured before deciding** (`hook-trace.log`, this node, ~25 h window, 726 trace lines, file at
147 KB against a 512 KB roll so the window is complete): **32 tag sends — 28 turn-end, 4 mid-turn
(12.5%)**. Mid-turn dispatch is genuinely load-bearing, and its loss would be silent at both ends.
One node, my own — not a fleet number.

**Operator ruling: HOLD.** This release ships the funnel work and keeps `tag_scan` as the only
parser. `[io].compliance` and the parser deletion move to a follow-up once #238 lands, and #22
stays open until then.

Consequently: **T5 and T7's retirements are OUT of this release**, T8 shrinks to nothing
user-facing (we keep teaching our own grammar because we still own it), and the cplugs skeleton
republish is **not** needed — no skill or brief text changes.

## Open questions (asked doyle; work everything else meanwhile) — BOTH ANSWERED ABOVE

- **Mid-turn tags.** We dispatch at PreToolUse today. AGENT_OUTPUT is documented as *the turn's
  closing output*, so a tag in a non-final assistant message may not be seen by core at all.
  Accepted capability loss until `releases#238` (mid-turn framed emission), or something we can
  cover? Must be stated in the CHANGELOG either way — a silent loss is not acceptable.
- **Parse vs the 16 KB cap.** Is shortform parsed over the full payload we pass, or over the
  truncated frame body? A tail tag dies silently in the latter. Do not truncate adapter-side either
  way (`truncated="1"` is core's follow signal).

## Tasks

- **T1 — schema refresh.** Vendor the node-local `manifest.schema.json` into
  `adapter/manifest.schema.json`; record provenance. `sh ci/manifest/check-manifest.sh` green.
- **T2 — manifest.** `[io] compliance = true`; `[io.now_signal] max_lines` (bound the per-poll
  context). No `only`/`without` narrowing — `only` would risk suppressing DISPATCH_RESULTS, the one
  outcome channel. New `REQ-IO-COMPLIANCE-DECLARED`.
- **T3 — USER_INPUT payload.** UserPromptSubmit's `state busy` carries `--payload-stdin` with the
  user's prompt. Reuse the v0.32.0 strict-with-fallback seam (`spt_strict_stdin`, refusal ⇒ re-report
  without the payload) — `env.spt` is exit-code-blind and a refused busy mark is the stuck-ACTIVE
  hazard by a new road. **Exactly one payload-carrying busy per turn**: the PreToolUse busy mark and
  `interrupt_watch`'s re-assert stay payload-free (core emits per call, not per transition, and does
  not deduplicate). New `REQ-IO-USER-INPUT-PAYLOAD`.
- **T4 — settle carry-over (T5's precondition).** Move the transcript settle into
  `turn_closing_output` so the payload read still waits out CC's final write. Repoint
  `REQ-HAZARD-TAG-STOP-SCAN-EARLY` at the payload read and retitle it — the hazard did not go away,
  its consumer changed. Test must fail if the settle is removed.
- **T5 — delete the local parser.** `tag_scan.rs`: the peer-dispatch half (finding 4).
  `hook.rs`: `scan_and_dispatch`'s send fan-out, the confirm build, the stamp probe, the outcome
  ledger, the QUEUED tripwire, `park_turn_end_confirm` + the UPS/PreToolUse surfacing of parked
  confirms + `tag_confirm_rel` state file. Keep the commune scan and its byte cursor
  (`REQ-COMMUNE-OUTPUT-SHORTCUT`, `REQ-HAZARD-TAG-FIRST-RUN-DROP` still apply to it).
  Our suppression guard goes with the parser — core owns the grammar; never leave two.
- **T6 — now-signal.** Replace the `api hint` call with `api now-signal <id> --session <sid>
  --user-input … [--agent-output …] --spec-manifest`, injected at UserPromptSubmit and PreToolUse
  (the cadence the verb is built for). Delta-only, so a quiet turn costs zero context; emit nothing
  when it prints nothing. **`--user-input` is argv, not stdin** — cap what we pass (Windows
  command-length limit); the cap is safe here (mention/monic scanning), and must NOT be applied to
  the state payload. New `REQ-NOW-SIGNAL-INJECT`.
- **T7 — requirements.** Supersede with `required_stages = []` + a title saying what replaced them:
  `REQ-TAG-PEER-MESSAGING`, `REQ-TAG-SEND-STAMP-EXPLICIT`, `REQ-TAG-SEND-STAMP-PROBE`,
  `REQ-TAG-SEND-FAILURE-LOUD`, `REQ-TAG-SEND-QUEUED-VISIBLE`, `REQ-TAG-SEND-VERDICT-NOT-CATCHALL`,
  `REQ-TAG-SEND-PRETOOL-TRACE`, `REQ-TAG-SEND-OUTCOME-LEDGER`, `REQ-TAG-CONFIRM-NEXT-HOOK`,
  `REQ-HAZARD-TAG-BODY-CODE-SPAN`. Keep and repoint `REQ-HAZARD-TAG-STOP-SCAN-EARLY`; keep
  `REQ-HAZARD-TAG-FIRST-RUN-DROP` and `REQ-COMMUNE-OUTPUT-SHORTCUT`. Keep the KNOWN-HAZARDS entries
  honest — a hazard that moved is edited, never deleted.
- **T8 — agent-facing text.** The SessionStart brief teaches the grammar in our own words
  (`adapter/strings/`): stop restating a grammar core owns, and stop promising a confirmation that
  no longer arrives (outcomes now surface via the now-signal). `docs/` + `docs-site/` are
  drift-gated — regenerate.
- **T9 — release.** Bump `adapter/claude-spt.toml`, CHANGELOG (state the cross-version story:
  agents on older adapters keep their local parser; core parses only for adapters that declare
  compliance). Gate, build three triples fresh, package, tag, release, mirror. **cplugs skeleton
  republish is expected this time** — T8 touches skill/brief text.

## Gate

`sh ci/run-gates.sh` PASS **re-run after the last file lands**, and `traceable-reqs check` exit 0 —
both before the commit. The two read-only ints are flaky under concurrent cargo; re-run alone before
believing a red.
