# M4 — PACER 0.4.0: verb refusals, stretch extend, class retirement

> JIT plan for milestone [#10](https://github.com/BigscreenVR/spt-pacer-tool/issues/10)
> (requests [#6](https://github.com/BigscreenVR/spt-pacer-tool/issues/6),
> [#8](https://github.com/BigscreenVR/spt-pacer-tool/issues/8),
> [#9](https://github.com/BigscreenVR/spt-pacer-tool/issues/9)). Version 0.4.0;
> `min_spt_core_version` stays `0.44.0` — nothing here needs newer core.

## Scope

1. **#6 — verb refusals.** A MAC-valid `shell_command` frame whose op or args fail to parse
   answers a one-line refusal to the bound endpoint instead of dropping silently. The MAC gate
   itself stays silent (`REQ-HAZARD-PACER-FRAME-MAC` — an unstamped line is an attacker, not a
   confused operator; it earns no oracle).
2. **#8 — `!!extend=<minutes>!!` + CLI twin.** Bridge the most recent stretch across idle/busy
   cycles: while a bridge deadline stands, an idle→busy edge does not close the stretch. One
   stretch, one id, one report carrying the full wall-clock span.
3. **#9 — `retire` verb, CLI only.** Remove one or more classes from the registry. No inline
   code form — registry surgery is an operator-scale act, not a turn-scale one.

## Rulings (posted verbatim to the issues)

### #6 refusal machinery

- Refuse only what the MAC accepted: unknown op (defense in depth behind core's vocab check),
  malformed/missing args, and **semantically dead verbs** — classify/extend of an id not in the
  pending window, retire of an unknown class.
- Refusal shape: one line, `⟳ **pacer** refused <op>: <reason>`, sent `--active-only` like every
  non-ping answer (a refusal must never wake anyone).
- Observed upstream note for doyle: core spooled a 1-arg `classify` against
  `classify = { args = ["id", "name"] }` without arity complaint — reported as an observation,
  not assumed to change; pacer refuses regardless of whether core later tightens arity.

### #8 extend semantics

- **State:** `bridge_until: Option<ms>` in the machine (in-memory — a restart already loses the
  running stretch, so a durable bridge would protect nothing).
- **Invoked while idle** (CLI path): the current stretch will not close before `now + minutes`.
- **Invoked while busy** (typical: the wake's report is in hand, the wait isn't over —
  `!!extend=15!!`): **reopens the most recently reported stretch** — its pending-window entry is
  the identity; the bridged stretch resumes from that stretch's original start. Its id stays; the
  final close **re-reports the same id** with the full span, superseding the fragment report.
- **Refused when:** no stretch to extend (nothing pending, nothing running), or the most recent
  stretch is already classified (classification is final — `#6` machinery answers).
- **Duration is wall-clock** from bridged start to final close. The busy blips inside a bridge
  are measurement noise inside one wait; the honest number is the span. (The alternative —
  summing only the idle fragments — claims a precision the 60s-floor world never had.)
- **Deadline behavior:** each extend sets `bridge_until = now + minutes` (a later extend
  supersedes an earlier one — latest intent, matching steering). While `now < bridge_until`, an
  idle→busy edge does not close the stretch. The first idle→busy edge at or after the deadline
  closes it normally: one report, total span, id per the reopen rule.
- **Pacing is untouched.** Pings and backoff run normally during a bridge — extend is
  measurement bridging, not wait suppression; `!!wait=m!!` exists and composes.
- **Replay:** extend codes ride the same acted-seq watermark as classify/steering
  (`REQ-HAZARD-PACER-CODE-REPLAY` family) — a re-read digest never re-extends.
- **Code form:** minutes only, 1..=1440 (a day); malformed or out-of-range → refusal (verb) /
  ignored like any malformed code (inline, codes have no reply channel).

### #9 retire semantics

- `retire = { args = ["names"] }` — one positional: space-separated class names (snake_case
  names cannot contain spaces, so the separator is unambiguous), e.g.
  `spt shell cmd PACER-0 retire "ci_run peer_wait"`.
- Removes each named class from the registry entirely; the answer line confirms what fell and
  what was unknown: `retired ci_run (~4m2s ×7); no class named peer_wai`. All-unknown → refusal.
- State file saved on any mutation; retire of a name mid-pending has no effect on pending ids
  (they are unclassified by definition).

## Tasks

1. `traceable-reqs.toml`: add + activate `REQ-PACER-VERB-REFUSAL`, `REQ-PACER-EXTEND`,
   `REQ-PACER-RETIRE` (stages: doc, impl, unit each). `CONTEXT.md` glossary: bridge, extend,
   retire, refusal. [doc]
2. Refusal machinery (#6): `frame.rs` decode returns a typed reject for MAC-valid-but-malformed
   lines; `main.rs` answers the one-liner `--active-only`. Unit: stamped-bad-args → refusal
   body; unstamped → silence (hazard tripwire extended). [impl+unit]
3. Retire (#9): `state.rs` retire fn + `frame.rs` parse_retire + manifest capability + answer
   line. Unit: known/unknown/mixed, persistence. [impl+unit]
4. Extend (#8): machine bridge state + digest `parse_extend` + frame verb + manifest capability;
   reopen path re-reports the same id with the superseding total. Unit: bridge holds across
   edges, deadline close, reopen identity, refusals, watermark no-replay, wait composition.
   [impl+unit]
5. Education line: status answer + education message mention extend and retire (one line each).
6. CHANGELOG `[0.4.0]`, bump `manifest.toml` + `Cargo.toml` to 0.4.0.
7. Gates: `cargo test`, `traceable-reqs check`, pack + schema-validate.
8. Release per `docs/RELEASE-RUNBOOK.md` (fresh triples, tag `v0.4.0`, gh release, digest
   verify). **First-ever `spt adapter update PACER`: check the update-wipe canary and the
   state file survive** (`REQ-HAZARD-PACER-STATE-WIPE-ON-UPDATE` — this IS the observation the
   canary was planted for). Restart instance, live E2E: one refusal answered, one bridged
   stretch reported whole, one retire round-tripped.
9. Milestone #10 states along the way: greenlit (plan committed) → wip (build) → acceptance
   (release verified) → done (E2E + canary check pass). Close #6/#8/#9 via the cascade, ruling
   comments first.

## Gate

Build + `cargo test` + `traceable-reqs check` green; release digest-verified; canary observed
surviving the update; milestone #10 at done.
