# NOW-SIGNAL #23 — W0 measure/verify report (todlando, 2026-08-30)

Measured at `aa9ac15a` unless stated. Blobs read out of the sha (`git show <sha>:<path>`), never
the worktree. No build in this wave. Ripe/not-ripe verdicts are the gater's; this is material.

---

## 1. VERIFY-DELIVERED — #11 core verb

Ticket ask → source, per ask. `crates/spt/src/api/nowsignal.rs` (1075 lines) unless noted.

| # | ticket ask | verdict | evidence |
|---|---|---|---|
| 1 | adapter manifests' hint system | **COVERED** | `gather_hints` :385; `Category::Hints => "HINTS"` :102 |
| 2 | monic system | **COVERED** | `gather_monics` :409; `"MONICS"` :104 |
| 3 | previous projects + activity dates + how-to-fetch project-context | **NOT COVERED — deferred by design** | no `PROJECTS` arm in `Category` :61-108. Precondition measured in §3 |
| 4 | instantiated shells | **COVERED** | `gather_shells` :526 — id, adapter name, effective status |
| 4b | *existing* shell adapters (+ bind notes?) | **NOT COVERED** | `gather_shells` :528 iterates `shellinfo::list_shells` — INSTANTIATED shells only. A registered-but-not-instantiated adapter has no row. This is #133's subject (`kind=shell` manifests gain `[hints]`, not-instantiated → one-line pointer) — flagged so it is not double-counted as delivered here |
| 5 | last-2-messages: time, relative-to-now, sender/destination, ~10-word excerpt | **COVERED, all four fields** | `gather_last_msgs` :542; line composed :563-570 carries `rfc3339_utc` + `relative_to_now` (:680) + peer + excerpt. The ~10 words is exact and ratified: `EXCERPT_WORDS = 10`, `spt-store/src/lastmsg.rs:36`, bounded at the WRITE (:17) not the render |
| 6 | file access helper | **NOT COVERED — deferred by design** | no arm in `Category`. Precondition measured in §4 |
| 7 | "maybe" edge transitions — x offline, y online, z joined subnet from node, node joined subnet | **PARTIALLY COVERED** | `gather_edge_transitions` :583. Online/offline arm carries id + status + node. Subnet-join arm (:591-601) renders `"{who} joined subnet {}"` — **no FROM-NODE**, and `who` falls back to a 12-char pubkey prefix when the member has no label. The ticket's "z joined `<subnet>` from `<node>`" is therefore half-rendered, and the endpoint-join vs node-join distinction is not visible in the line |
| 8 | `--spec-manifest` / `--spec-file <json-spec-filepath>` | **COVERED** | `resolve_spec` :696; flags at `api/mod.rs:484-490`. `--spec-file` precedence over `--spec-manifest`, unreadable file reads as absent (:698-701) |
| 9 | each category an XML tag nested under `<SPT-NOW-SIGNAL>` | **COVERED** | `compose` :143 |
| 10 | title's `--user-input-json` / `--agent-output-json` | **COVERED IN SUBSTANCE, DIFFERENT SPELLING** | shipped as `--user-input` / `--agent-output` (`api/mod.rs:478-482`), plain strings not JSON. Naming and payload shape both differ from the title. Worth a ruling: ratified-elsewhere, or an unclosed ask |

**Extra beyond the ticket** (not a gap, recorded so the census is honest): `DISPATCH_RESULTS`
(:107, `gather_dispatch_results` :630) — minted by the IO-PARSER milestone, not asked for here.

**Recommended issue comment:** delivered with three named exceptions — 4b (registered-but-not-
instantiated shell adapters → #133), 7 (subnet-join line lacks the from-node and the
endpoint/node distinction), 10 (flag spelling/shape vs the title). Asks 3 and 6 are the
deferred-by-design pair (#16/#17) and should be closed against those, not against #11.

---

## 2. VERIFY-DELIVERED — #14 ENDPOINT_MENTIONS

| ticket ask | verdict | evidence |
|---|---|---|
| confirm each matched endpoint's existence | **COVERED** | `gather_endpoint_mentions` :441 emits a line per matched row |
| current online status | **COVERED** | line composition :470-474 |
| endpoint description | **COVERED** | :481-486 |
| the endpoint's node | **COVERED** | :474 |
| description omitted after first occurrence in session | **COVERED, and it is the description SPECIFICALLY** | separate `desc_key = "{id}#desc"` guarded by `seen.take_new` :482-484, distinct from the row's own presence key :467. That is exactly the ticket's "some elements are omitted if it's not the first occurrence — specifically, the endpoint description" |

**Extra beyond the ticket:** shared-subnets (`shared_subnets` :497, appended :477-479), added by
the CONTEXT ratification. Truthfully empty when either node's membership is unknown (:499-501).

**Recommended issue comment:** delivered, all five asks, no exceptions. #14 is the cleaner of the
two closes.

---

## 3. PRECONDITION — #16 PROJECTS ("project-index cross-node semantics")

The precondition is **PARTIALLY satisfied**, and the gap is specific.

- **Carried today:** `recent_projects` rides the presence gossip —
  `spt-daemon/src/registryhost.rs:618` (`recent_projects_for`), attached at :652, capped by
  `MAX_GOSSIPED_PROJECTS = 8` (:1035). Derived from the endpoint's `sessions.log` cwds,
  spt-internal excluded, deduped by raw cwd before project-id derivation (:1056-1066).
- **The type is `Vec<String>` — names only.** So of the ticket's three asks:
  - *previous projects* — **satisfied** (identity, freshest 8; recency ORDER is implied by the
    `.rev()` scan at :1074, but order is not a date);
  - *activity dates* — **NOT satisfied**: no timestamp is carried on the wire for a project;
  - *how-to-fetch any project's `project-context`* — **NOT satisfied**: no fetch-guidance
    mechanism measured anywhere; nothing tells a reader how to pull a remote project's context.

**Material for the ruling:** a PROJECTS category built on today's gossip could render *which
projects, freshest first, on which node* — genuinely useful and already cross-node. It could not
render an activity date without widening the gossip record, and could not answer "how to fetch"
without a verb that does not exist. Whether that is ripe depends on whether the category is
allowed to ship its first third.

---

## 4. PRECONDITION — #17 FILE_ACCESS_HELPER ("off-node file-transfer UX state")

**What exists:**
- `crates/spt-net/src/net/xfer.rs` — real off-node transfer: chunked, progress-queryable,
  bidi-QUIC, `Fetch` (pull off target) / `Push` (onto target), idempotent by absolute chunk
  offset, atomic-rename commit, `xfer_id` the durable end-to-end key.
- `spt shell send <ref> --file <path>` (`cli.rs:23004-23072`) — writes into the shell perch's
  `files/` dir and composes a file frame; progress via `spt_store::xfer::XferProgress`.

**The gap, and it is an AXIS gap rather than a missing feature:** the xfer module's own doc says
it moves files "between an instance's node and the node **driving** it" — the REMOTE-CONTROL
axis, keyed to an rc session. #17 needs a different axis: *the attached user sits on node N and
names a filepath; how does THIS agent retrieve it?* Measured negative: `spt send` carries no
`--file` (its flag list, `cli.rs:103-126`, is delivery-window flags only), so there is no
endpoint→endpoint file movement at all.

**Material for the ruling:** the transport exists and is proven; what is missing is an
addressing story (endpoint→endpoint rather than driver→target) and therefore the very sentence
the ticket wants to emit — the ticket's own `[??? what's the method]` is still unanswered at the
product level, so the category cannot yet name a method that works.

---

## 5. #113 echo-commune cadence — legacy vs spt-core DELTA

Legacy read from `../claude_skill_owl` (operator-authorized, BEHAVIOUR only; ADR-0001 keeps
wire/schema copy-verbatim, everything else clean-room). The ticket states four remembered facts;
all four measured, **three confirmed and one corrected**.

| # | ticket's memory | measured | delta |
|---|---|---|---|
| 1 | "each user output **or tool call** boundary creates the more-done sentinel" | **CORRECTED.** Sole writer is the **Stop hook** — `src/owl/hook_idle.rs:91` (`spawn_echo_commune_if_live`), doc at echo_fire.rs:3 "The Stop hook (src/owl/hook_idle.rs) writes a `.more-done` sentinel", D-02 zero-byte file, D-04 `OWL_ECHO_COMMUNE=1` recursion guard. There is NO tool-call-boundary writer — the other hooks (`hook_prompt`, `hook_check`, `hook_subagent_*`) do not write it | **turn-end only, not per tool call** |
| 2 | "fires once the sentinel reaches 15 minutes old" | **CONFIRMED, exactly.** `ECHO_COMMUNE_WINDOW = Duration::from_secs(15 * 60)`, echo_fire.rs:82, "D-07: hardcoded 15-minute gating window. Not configurable in this phase." `should_fire_decision` :124: absent → `SkipNoSentinel`; age ≥ window → `Fire`; younger → `SkipSentinelFresh { remaining }` **and the caller schedules a short next pulse to wake near the crossing**. Age from birthtime with mtime fallback (:112-116) — "oldest un-fired Stop" where birthtime works, "most-recent Stop" where it does not, a declared degradation. Metadata failure / clock skew → **fires** (fail-open, :129) | spt-core has **no age gate at all** |
| 3 | "echo communes are NOT surfaced to the live agent's running context (spt-core currently does)" | **CONFIRMED on both halves.** Legacy: the cadence path builds argv with `force=false, forward_to_self=false` (echo_fire.rs:255, and :601 "Cadence-path argv: NO flags") — `--forward-to-self` is reserved for the other paths. spt-core: `spt-live/src/echo.rs:170` calls `record_context_injection(id, KIND_ECHO_MIRROR, …)`, whose own comment is "record that spt **mirrored an echo-commune brief into the agent's context**" | **this is the real behavioural divergence** |
| 4 | "also fire at a session boundary (clear, compact, harness-offline) and catch the delta to the boundary edge" | **CONFIRMED for clear/compact; harness-offline UNVERIFIED.** `build_inner_argv` takes `source: Option<&str>` = `Some("clear") | Some("compact")` (echo_fire.rs:327-331) and selects the RE-EMIT prompt for post-`/clear` re-hydration; `None` is the cadence/orphan path. I did not find a harness-offline arm and am reporting that as unmeasured rather than absent | spt-core fires on **transition edges** instead |

**spt-core's current cadence, measured:** the gate is armed by TRANSITION EDGES only —
`resting.rs:365 arm_transition_echo`, fed by the attach arm's detach, the registry arm's
attention shift, and `spt suspend` — then consumed read-and-clear by the endpoint's own pulse
loop (`spt-live/src/pulse.rs:82 take_echo_gate`, `DEFAULT_PULSE_PERIOD = 5s` :31). Presence-only:
no age is read, so there is no window, no near-crossing scheduling, and no fail-open arm because
there is no metadata read to fail.

### The forks, for the gater

1. **TRIGGER.** Legacy echoed off *work happening* (every turn end, age-gated). spt-core echoes
   off *attention changing* (detach / attention shift / suspend). Consequence, and it is the
   load-bearing one: **an agent that works for hours without a transition edge never echoes in
   spt-core**, where legacy would have echoed every ~15 minutes. That is a coverage difference,
   not a cadence tuning difference.
2. **SURFACING.** Legacy's cadence echo did not reach the running agent; spt-core mirrors it in.
   The ticket asks for the legacy behaviour, and CONTEXT grounding is owed here — the mirror is
   load-bearing for the two-origin digest tap (`REQ-TERM-7`), so removing the surfacing is not a
   one-line deletion and should be ruled, not assumed.
3. **BOUNDARY DELTA.** Legacy caught the delta from the last echo to the boundary edge via the
   `--source` re-emit path. spt-core's boundary handling (`api boundary`) rotates the session and
   appends the ledger; whether it catches an echo delta is **not measured here** and is the next
   measurement I would run if this member goes to build.
4. **CONFIGURABILITY.** The legacy window was explicitly "not configurable in this phase". If
   spt-core adopts a window, whether it is a constant or a manifest/spec knob is a decision the
   legacy code deliberately did not make.

**No build performed. No fork resolved.** All four are yours.
