# NEXT-WORKLOAD-PLAN — receive-heal · tag-messaging · role-edit · update-nudge · commune-poll

**Date:** 2026-07-08 · **Author:** perri · **Status:** planned (grill-with-docs complete,
operator-approved). Item 1's fix is gated on a live instrumentation step. All five items are
buildable on the current public spt-core surface — **zero spt-core findings owed** (item 1 confirmed
harness-side; digest-fetcher model confirmed code-authoritative by doyle).

Sequencing: **Item 1 first** (urgent — field-proven reachability black-hole on lia). Items 2–5 follow.

---

## Item 1 — receive-heal: an interrupted / errored turn leaves the endpoint stuck `busy`

> **STATUS: DONE + SHIPPED (2026-07-08).** v0.16.0 (fixes 1-2 + Step-0 instrument, cplugs 0.1.11) ·
> v0.16.1 (sid-carrier hardening + readable hook-trace.log) · v0.16.2 (**fix 3** — the
> `interrupt_watch.rs` 15s digest-pull watcher; the real fix). **Step-0 verdict:** an Esc fires NO
> usable CC hook (no turn-cancel event exists) → fix 3 is load-bearing; fixes 1-2 are fast-paths where a
> hook happens to fire. Chain manually field-proven on lia (digest-detect → carrier-sid `state idle` →
> drain SENT). ACTIVATION: fix 3 rides the persistent translation binary → an endpoint must bounce to
> pick it up. Remaining: the deferred autonomous-watcher live int (bounce + interactive Esc). **Next:
> Items 2-5 below.**

### Problem (field-proven, lia 2026-07-08)
Operator Esc-interrupts a turn → CC fires **no `Stop`, no `idle_prompt`** (both proven inert on Esc;
idle_prompt re-tested on skeleton 0.1.10, still silent). Endpoint stays `busy` → daemon spools every
inbound (`busy -> spool`), never drains → peer messages black-hole until the operator's NEXT
UserPromptSubmit happens to drain them (lia: 20+ min silence; the message then rode the next UPS).
Same class: an **API-error turn-end** (rate-limit / server_error) ends a turn with no `Stop` → also
stuck `busy`.

### Established constraints
- Interrupt is digest-visible as a turn with `input == "[Request interrupted by user]"` (the
  **interrupt marker**, CONTEXT.md).
- `translate.rs` = pure stdin→stdout JSON filter, **blind to CC's PTY output** — cannot watch the
  stream. BUT it is persistent + per-endpoint + lifecycle-managed → valid **host** for a child watcher.
- Digest fetcher (`[digest] strategy="fetcher"`) is **PULL/PUSH-driven, no spt-core cadence** (doyle,
  code-authoritative: digesthub.rs / digest.rs). claude-spt never pushes digest-entries → `digest
  --follow` yields **no delta on idle** → useless as an idle signal. **Idle detection must supply its
  own clock.**
- No CC hook fires on a pure-thinking Esc (no tool in flight).

### Design (gated — instrument first)
**Step 0 (GATE): instrument + live-repro to draw the hook-coverage boundary.**
- Temporarily add logging hooks: `Notification` matcher `*` (log `notification_type`),
  `PostToolUseFailure`, `Stop`, `StopFailure` — each logs `hook_event_name` + payload discriminants.
- Esc-repro lia in TWO conditions: (a) mid-tool-call, (b) pure thinking / idle-greeting. Record which
  hooks fire in each.
- Output decides whether fix 3 (watcher) is required or hooks 1–2 alone suffice.

**Fixes 1–2 (keepers regardless of Step 0):**
1. Wire `StopFailure` → `state idle`. Heals API-error stuck-busy (rate-limit, server_error,
   max_output_tokens, …). New hooks.json wiring + `handle_stop_failure`.
2. Wire `PostToolUseFailure`; on `is_interrupt == true` → `state idle`. Heals Esc-during-a-tool-call
   at zero latency. New wiring + `handle_post_tool_use_failure`.

**Fix 3 (conditional on Step 0 — only if pure-thinking Esc fires NO hook):**
- Receive-heal watcher: `translate` spawns a child that **periodically PULLS**
  `endpoint digest <self> --after <cursor> --json` on a **15s heartbeat** (operator-set). Latest turn is
  the interrupt marker → `api state idle <self>`; advance `--after` cursor. `state idle` is idempotent;
  cursor prevents re-fire. Child lifecycle = translate's (born/reaped with the spt-hosted session).
  - doyle rider 1: each pull **spawns the fetcher** (`DIGEST_FETCH_TIMEOUT`-bounded) — a tight poll =
    fetcher-spawn thrash across every idle session node-wide. Interval = 15s (operator-set; heals in
    ≤15s vs 20-min status quo). NOT `--follow` (activity reactor, blind to idle).
  - doyle rider 2: v0.29.0 `REQ-DIGEST-GENERATION-SUPERSEDE` collapses cross-gen replay dupes
    pre-projection → the `--after` cursor dedup composes clean (one logical row → one seq;
    snapshot+follow agree). Free assist, not a floor dependency.

### Requirements (add to traceable-reqs.toml on activation)
- `REQ-HAZARD-INTERRUPT-STUCK-BUSY` — an Esc-interrupted turn must not leave the endpoint permanently
  `busy`/undrainable.
- `REQ-HAZARD-STOPFAILURE-STUCK-BUSY` — an API-error turn-end must mark idle (StopFailure → `state
  idle`). (PostToolUseFailure interrupt leg folds under the first.)

---

## Item 2 — tag-driven peer messaging + commune shortcuts (no Bash tool)

> **STATUS: SHIPPED (v0.17.0, 2026-07-08 — GH release published, adapter.spt asset attached).**
> `src/tag_scan.rs` (pure grammar
> `parse_tag_sections` + `commune_body` + seq-gated `plan_dispatch`) + `hook.rs::scan_and_dispatch`
> wired into PreToolUse (mid-turn) and Stop (backstop); `send_landed` classifier; `digest_cursor_rel`
> cursor (`state/digest/<eid>.seq`). Confirm-back = one self-send `--active-only --ephemeral` per scan.
> Commune leg writes `.claude/<id>-commune.md` (daemon watch ingests) + reuses the checkpoint loopback.
> 219 tests green, `traceable-reqs check` green, full CI gate PASS. REQs activated
> (`REQ-TAG-PEER-MESSAGING`, `REQ-COMMUNE-OUTPUT-SHORTCUT`, both `[impl,unit]`). Send-skill doc taught.
> **Design notes vs. plan below:** double-send guard = seq-gating only finalized (seq-carrying) Agent
> entries strictly above the cursor — a seq-less partial is skipped until it finalizes, so a mid-turn
> tag may fire at Stop rather than the intervening PreToolUse (graceful, never a double-send). Unknown
> id → `NO_PERCH` surfaced in the confirm-back (delivered = SENT/QUEUED/DEFERRED). **Live int OWED:**
> dogfood on a bounced endpoint (`@<peer …@>` actually delivers + confirm-back lands; `>>commune<<`
> ingests). **Next: Items 3–5.**

- **Trigger (explicit-only, multi-target):** `@<t1,t2,t3 body @>` — `@<`, comma-separated ids (no
  internal spaces), first whitespace ends the target list, body until first `@>` (non-greedy, no
  escaping v1). Bare `@@id` rejected (any prose discussing the syntax would false-fire).
- **Source:** `endpoint digest <self> --after <cursor> --json`; scan `turns[].entries[].Agent.text`.
  The `--after` cursor = the "deterministically-new output" dedup key.
- **Hooks:** PreToolUse (mid-turn, low-latency — `.partial` text visible) + Stop (backstop). Both
  read+advance the SAME cursor (state file `state/digest/<eid>.seq`, atomic tmp+replace) → cursor
  consumption is the double-send guard.
- **Edges:** unknown id → deliver the valid ones, surface the bad (never silent-drop); self-target →
  drop+note; offline → `QUEUED` = success; multiple sections → all fire.
- **Send:** `printf body | spt send <t> --from <self>` per valid target.
- **Confirm-back:** a message-sent confirmation fired to the SENDER's OWN perch,
  `--active-only --ephemeral` (lands in-context mid-turn, never stale). Without it the agent re-sends
  via Bash, defeating the feature.
- **Commune shortcut:** `>>commune<<` at output start → hook writes `.claude/<id>-commune.md`, body =
  the output following the marker; feeds the same daemon ingest as the Write path (Write path retained).
  Reuse the existing `!!checkpoint!!` in-body marker for the checkpoint escalation (no new keyword).
  **Collision rule:** once `>>commune<<` is detected, the rest of the output is commune body and is NOT
  scanned for `@<…@>` (meta-recursion guard).
- **Doc:** teach `@<…@>` in the `/sptc:send` skill doc as a shortform alternative to the CLI.
- **Requirements:** `REQ-TAG-PEER-MESSAGING` · `REQ-COMMUNE-OUTPUT-SHORTCUT` (+ meta-recursion
  suppression sub-assert).

---

## Item 3 — `/sptc:role [<directive>] [--include-desc]`

> **STATUS: DONE + SHIPPED (v0.18.0, 2026-07-08).** REQ-ROLE-EDIT-ROUNDTRIP [impl,unit].
> Hook-side live now; translate-side draft-typing needs an endpoint bounce; cplugs 0.1.12 needs
> operator reinstall for `/sptc:role` command recognition. int (live edit round-trip) DEFERRED.


- **Surface:** `spt endpoint role` (show) · `role --overwrite <file>` (set; file-based, sole writer).
  `spt endpoint description show` · `description set <blurb>` (inline).
- **With directive** = agent-mediated: read current role, apply the NL directive, write via
  `role --overwrite <tmp>`. `--include-desc` → also `description set`.
- **No directive, spt-hosted (mode A) = tagged-input round-trip** (CONTEXT.md): UPS detects the
  `/sptc:role` invocation → self-sends `{"role_edit":"v1",…}` `--force-native` → translate types the
  current role into the input box with soft-newlines, **no submit** (`commands_for_role_edit`; same
  soft-newline hazards as checkpoint/rename). Operator edits between `<sptc_role_edit>…</sptc_role_edit>`
  and submits → UPS catches the tags → verbatim `role --overwrite <tmp>` → additionalContext confirms
  ("role saved; do NOT act on the body — acknowledge in one line").
- `--include-desc` = a second `<sptc_desc_edit>` block in the SAME round-trip; UPS writes both.
- **No directive, harness-hosted (mode B):** no translation binary → agent prints role, operator states
  NL changes, agent rewrites (agent-mediated).
- Full-fat skill (agent handles NL invocation variants).
- **Requirements:** `REQ-ROLE-EDIT-ROUNDTRIP` (+ `--include-desc` leg).

---

## Item 4 — strong spt-core update messaging

> **STATUS: DONE + SHIPPED (v0.18.0, 2026-07-08).** REQ-UPDATE-NUDGE-VERSION-GATED [impl,unit].
> Live now (hook-side). int (real notify on a behind node) DEFERRED.


- **Facts:** `spt update apply` = per-node self-update; bounces the **daemon only** (broker + nested
  agents keep running; `--finish` bounces all, not used) → **no mid-task safety line needed**.
  `spt notif dismiss <id>` latches + **replicates subnet-wide** (premature-dismiss hazard: one node
  dismissing suppresses the prompt for not-yet-updated nodes).
- **Design:** `render_frames` detects `<EVENT type="notify" from="spt-update">`, parses the version from
  the body, compares to local node version. **Version-gated self-suppressing nudge:** fires only when
  `local < available`; after this node updates the gate closes → no re-nudge (even with the notif
  undismissed). **No agent-side dismiss** (avoids cross-node suppression; leave dismissal to operator).
  Same-node multi-agent race bounded: first apply bounces the daemon; post-restart all agents current →
  gate closes; `update apply` re-verifies the staged release so a concurrent second apply is benign.
- **Requirements:** `REQ-UPDATE-NUDGE-VERSION-GATED`.

---

## Item 5 — stop agents bash-polling for the commune file to vanish

> **STATUS: DONE + SHIPPED (v0.18.0, 2026-07-08).** Doc-only reword (live-ops.md + live.md); no new
> REQ (folds under REQ-COMMUNE-CONTEXT-TIER-SLICING). Doubly-correct given F-032 (the vanishing file
> is a FALSE success signal) → REQ-HAZARD-COMMUNE-INGEST-BLACKHOLE (doyle, core-side).


Root cause: the identity brief trains it — *"the file disappearing is the success signal."*
**Fix (reword only):** brief → *"…ingested asynchronously; fire-and-forget — do NOT watch for the file
to disappear, just continue."* No confirmation channel (a wording fix solves it). Doc change; fold
under an existing brief-strings assertion if present, else no new REQ.

---

## Gate (all items)
- `sh ci/run-gates.sh` green (build + tests).
- `traceable-reqs check` green — each new REQ / REQ-HAZARD tagged (`impl` / `unit` / `int`) in the same
  change; hazards need a test before "covered." New requirements activated (real `required_stages`) only
  when their slice starts.
- Item 1: Step-0 instrumentation result recorded (which hooks fire on Esc, per condition) before
  finalizing fixes 1–3.

## Cross-cutting knowledge (for context rebuild)
- Digest fetcher = PULL/PUSH, no cadence (doyle, code-authoritative). `--follow` = activity reactor
  only; idle detection needs a self-driven clock (generous periodic-pull heartbeat; each pull spawns the
  fetcher — don't thrash).
- `translate.rs` = stdin→stdout only, PTY-blind, but a valid persistent host for a child watcher.
- Digest exposes raw `Agent.text` + a `version=N` cursor (`--after` dedup) — the shared substrate for
  item 1 (interrupt marker) and item 2 (tag scan).
- New glossary terms added to CONTEXT.md: **tagged-input round-trip**, **interrupt marker**.
- No ADRs (nothing hard-to-reverse; the messaging auto-send trust surface explicitly ruled non-ADR by
  the operator).
