# Recharge milestone — nomenclature, session color, trust auto-seed, two reachability gaps

## Status (2026-09-09)

Shipping as **v0.39.0**. Task 3 (#21) and Task 2 (#23) landed in `7e39462`; Tasks 6 (#34), 1 (#24)
and 4 (#31) landed after it. `ADR-0011` carries the #24 ruling — the plan called for ADR-0010, but
that number was already taken by the account-roots decision.

**Task 5 (#32) ships nothing and is the first work of the NEXT milestone**, deliberately. Its
measurement needs Task 6's retention fix *in the field*, which needs this release; and its stated
root cause is refuted with no characterised replacement, so building on the narrative below would
be a guess wearing a requirement's clothes. What is established: candidate (b) is REACHABLE (driven
by hand, no `SPT_ENDPOINT_ID` → a TRACE line with no `busy=` stage at all), which is NOT the same
claim as (b) firing in the field — that is what the trace retention is for.

> JIT plan (AGENTS.md §Plans). Scope = the five bagged Requests moved to EVAL on 2026-09-09 and
> attached to this milestone in the private home. Gate for every task:
> `sh ci/run-gates.sh` PASS **and** `traceable-reqs check` exit 0.
>
> **Already excluded from the public mirror** — and deliberately with NO edit to
> `ci/publish/mirror-public.sh`. The root JIT-plan class is excluded there as a PREDICATE
> (`:(glob)*-PLAN.md`), not as a list, precisely so a new plan naming private board coordinates is
> covered by construction rather than by someone remembering to add a line. This file names
> private-home issue numbers and is covered by that predicate on its name alone.

## Where this came from

Five Requests bagged by the operator, pulled from the Discord bag on 2026-09-09 and grilled the
same day. Bag read via `alchemy-1 bags`; bodies via `view`. `#1 Spill-to-file tax` was **left out
by operator ruling** — it is open, untyped, and squarely ours, but its own finding ("cap tuning
recovers only 6%") implies a redesign rather than a tweak, so it stays in Backlog for a future bag.

Operator decisions taken at the grill (recorded as comments on each Request; this plan is the build
order, not the rationale):

- **#23** — the title and body were not in conflict; they name **different profiles**.
  `claude-spt` → purple, `claude-spt:ccs` → orange, `claude-spt:alt` → green.
- **#24** — **staged retirement, not a hard cut.** `recharge` becomes the name everywhere;
  `commune across` and `--across` stay silently accepted this release. Retirement is a later
  stage, once peers' durable context has rotated. Same discipline as the wake rename.
- **#32** — the stated root cause is **refuted** (below). EVAL measures before anything is built.

## Task 1 — #24 `commune across` → `recharge` (staged)

**Surface:** 107 occurrences across 21 files (`commune[ -]across|across[ -]commune|--across`,
case-insensitive). Weight is in `tools/claude-spt/src/hook.rs` (22), `traceable-reqs.toml` (19),
`adapter/claude-spt.toml` (9), `docs/KNOWN-HAZARDS.md` (8), `CHANGELOG.md` (7), `CONTEXT.md` (6),
plus the skill bodies and briefs.

**What changes.** Every *agent-facing and user-facing* name becomes `recharge`: the
`/sptc:commune` skill body, `adapter/strings/briefs/*`, `CONTEXT.md`, the manifest's brief text,
and the hook's log lines. The skill gains `--recharge` as the primary flag spelling.

**What does NOT change this release (the staged half).** `--across` keeps working, and a brief or
commune that says "commune across" is still understood. Nothing is refused. Historical records —
`CHANGELOG.md` entries already shipped, `docs/adr/`, prior `docs/plans/*` — are **left alone**: they
describe what shipped under the old name, and rewriting history would make the release notes lie.

**Traceability caution.** `traceable-reqs.toml` holds 19 occurrences. Requirement *ids* are the
registry's identity — renaming an id is not a rename, it is a delete plus an add, and it orphans
every tag pointing at it. **Ids stay; only descriptions change.**

**ADR owed.** `docs/adr/0009-across-wake-vocabulary-split.md` is the standing vocabulary ruling.
This supersedes its naming half → mint **ADR-0010**, recording the staged retirement and naming the
release that may complete it.

## Task 2 — #23 session color after `/rename`

**Emit site:** `translate.rs:260` — `v.push(json!({ "text": format!("/rename {}", …) }))`.
The `/color` command MUST ride the **same combined sequence** as the `/rename`, never a second one:
two post-clear sequences blend (`boundary-rename-v0141`, a bug already paid for). The stash/fire
split at `translate.rs:581` is where a blank name stashes nothing — the color follows the same rule,
so a stash that emits no rename emits no color either.

**Profile → color** resolved from the adapter/profile identity already available at the emit site:

| profile | color |
|---|---|
| `claude-spt` | purple |
| `claude-spt:ccs` | orange |
| `claude-spt:alt` | green |

An unknown profile emits **no** `/color` — never a guessed color, and never a stray bare `/color `.

## Task 3 — #21 reachability notice covers `Task Output`

**Not a new mechanism.** `reachability_notice(Nudge)` (`hook.rs:2510`) already has three variants —
`AgentSpawn`, `LongBash`, `General` — dispatched at `hook.rs:3813-3816`. This adds a fourth,
`Nudge::TaskOutput`, on the tool that blocks a turn waiting for a background task to report.

Same two standing limits apply and are **not** re-litigated: the notice is **advisory** (operator
ruled out deny-and-steer), so it shapes the *next* call, not the one it fires on; and the
three-per-turn cap stays.

## Task 4 — #31 auto-seed Claude Code's directory trust store

**Ask:** if an endpoint's target cwd is absent from the trust store, or has
`hasTrustDialogAccepted == false`, set it true — so an endpoint cannot come up hookless and blocked
on a dialog no human is watching.

**Three invariants this must not re-break, all already paid for:**

1. **Never open-truncate `.claude.json`** — an ENOSPC mid-write wiped trust for 16 projects.
   Write via tmp + `os.replace` (`claudejson-write-hazard`).
2. **Trust is per-config-root.** The write must target the *endpoint's resolved* config root, not
   this process's (`f027-trust-seam-groundtruth`).
3. **Trust alone is not enough to clear startup.** External-`CLAUDE.md`-imports is a *second*
   dialog that hangs a spawn just as hard (`headless-spawn-external-imports-gate`). Seed both, or
   the feature only half-delivers its own promise.

Write **only when the value is actually wrong** — no rewrite on the happy path, which keeps the
blast radius of invariant 1 near zero.

## Task 5 — #32 the busy race (EVAL FIRST — no build until measured)

**The stated root cause is refuted.** UserPromptSubmit *does* mark busy:
`report_busy_with_input` (`hook.rs:3055`), and live traces carry a real `busy=` stage
(`busy=36ms` on this session's own UPS). So "UPS doesn't mark busy" is not the defect. The operator
flagged the stated cause as suspect; it is.

**Two candidates, neither yet measured:**

- **(a) A race, not an absence.** The busy mark lands one binary spawn plus ~36 ms after Enter.
  Anything core pushes in that window is PTY-injected into a turn that is already running. This
  fits the report's wording ("directly after a UserPromptSubmit") exactly.
- **(b) A silent hole.** The whole busy/poll block is guarded by `if !id.is_empty()`
  (`hook.rs:3044`). An identity resolution that comes back empty marks **nothing** — and reads
  identically to a healthy turn from outside.

**Measurement before design** (the ISOLATE-BEFORE-FILING rule, and a verdict may not assert more
than it measured):

1. Stamp the wall-clock gap from UPS hook entry to the `state busy` call returning, on a real
   endpoint. That is (a)'s window, in milliseconds.
2. Replay the io funnel across a turn where a message landed near a prompt submit, and read whether
   the inbound was pushed or spooled.
3. Count `id.is_empty()` occurrences in `hook-trace.log` — (b) is real only if it ever fires.

Only then choose the fix. **If measurement shows the window is real**, the direction is to move the
busy mark to the earliest point in the hook that has an id, ahead of every other stage — but that
is a conclusion this plan has not yet earned, and it will not be built on the strength of the
narrative above.

## Task 6 — #34 the trace instrument (filed DURING this milestone, blocks Task 5)

Filed 2026-09-09 while attempting Task 5's measurement, which could not be made: the
`TRACE UserPromptSubmit` line needed was written and then destroyed roughly two minutes later by a
peer endpoint's hook crossing the cap.

Measured on HFENDULEAM, not estimated: fill rate **87 B/s**, 512KB cap reached in **101 min**,
**247 of 263 lines (95% of bytes)** are stale-frame replay. Two defects:

1. **The spam.** `arm_from_commune_frame` logs one line PER STALE FRAME PER POLL, per endpoint, and
   the funnel replays history on every poll. One summary line per poll ("N frames refused as older
   than this session, newest at_ms X") carries the same value. Collapsing it alone takes retention
   from 101 min to ~32 h.
2. **The roll is a truncation.** At the cap the file is emptied (`hook.rs:4572`), not rotated —
   and by whichever endpoint's hook crosses the threshold, so one agent's poll destroys another's
   evidence. Keep one previous generation.

This is a prerequisite, not a duplicate of #32: #32 owns the busy-window defect, this owns the
instrument that must be trusted to characterise it.

## Requirements to mint (registry first, then satisfy)

| id | stages | covers |
|---|---|---|
| `REQ-RECHARGE-VOCABULARY` | doc, impl, unit | Task 1 — `recharge` is the name; `--across` / "commune across" still accepted; ids untouched |
| `REQ-SESSION-COLOR` | impl, unit | Task 2 — per-profile `/color` in the SAME sequence as `/rename`; unknown profile emits none |
| `REQ-TRUST-AUTOSEED` | impl, unit | Task 4 — seed trust + external-imports at the endpoint's resolved root, tmp+replace, write only when wrong |

`REQ-DIST-REACHABILITY-NOTICE` is **amended, not replaced** (Task 3) — its text states the set of
tools that earn a targeted nudge, and that set is what changes. Read its current wording before
editing: if it asserts an invariant this breaks (as it did when Bash was added), the title must be
rewritten, not merely extended.

Task 5 mints **no requirement yet** — the defect is not characterised. Its requirement is written
with the fix, once the measurement names which defect is being fixed.

## Order and gate

1. Registry entries (three new, one amended) — **before any code**.
2. Task 5's **measurement** — first, because it is the only task whose scope is still unknown, and
   a surprise there changes what this release contains.
3. Task 1 (rename, staged) — largest diff, lands early so the rest builds on final vocabulary.
4. Task 2, Task 3, Task 4 — independent of each other.
5. Task 5's fix, scoped by step 2.
6. `sh ci/run-gates.sh` PASS + `traceable-reqs check` exit 0 → commit.
7. Release per `docs/RELEASE-RUNBOOK.md`. **Structural plugin change** (the skill body and flag
   change in Task 1) ⇒ this milestone **does** need the cplugs skeleton step
   (`ci/publish/package-skeleton.sh` + `claude plugin install sptc@cplugs`).
8. `alchemy-1 release <tag>` — promotes the shipped Acceptance Requests to DONE.

## Deferred, deliberately

- **Retiring `--across` / "commune across".** Stage 2 of Task 1, a later release. The wake rename
  took three stages and shipped its retirement 40 releases late; the lesson recorded there is to
  grep `ci/` and `tests/` for the token before retiring it, not just `src/`.
- **`#1 Spill-to-file tax`.** Operator ruling: out of this milestone. Still open, still ours, and
  still untyped — its Type wants filling so it stops rendering as `[?]`, but Type is set once and
  never changed, so it is filled deliberately or not at all.
