/grill-me over M8, then execute once open decisions are made


436	required_stages = ["doc", "impl"]  # doc activated M6-D4 (Tier-1 corpus, agent audience first-class on the landing page); impl activated M6-D5: the second depth is machinery — xtask `site` publishes raw .md alongside every HTML page (/x.html ↔ /x.md), generates llms-full.txt from SUMMARY order, places llms.txt/schema/scripts at stable site-root URLs, ships rustdoc; docs-publish.yml is the tag-triggered pipeline to the spt-releases gh-pages branch (rendered snapshot; truth + history stay here, ADR-0014)
437	[[requirements]]
438	id = "REQ-DOCS-2"
439	title = "Sub-10-minute runnable killer quickstart per audience"
440	required_stages = ["doc", "int"]  # activated M6-D4: both killer quickstarts authored with REAL captured outputs, zero placeholders (messaging: install→two agents message incl. the QUEUED/backlog beat; adapter: walked via the shipped mock adapter, every command verified against the binary). int = quickstart_e2e.rs runs the published messaging flow step for step in the normal CI sweep — the doc cannot silently rot
441	[[requirements]]
442	id = "REQ-DOCS-3"
443	title = "Diátaxis structure; one canonical way to do X"
444	required_stages = ["doc"]  # activated M6-D4: the corpus IS the structure — per-vertical overview/tutorial/how-to/reference separation (SUMMARY.md IA per DOCS-STRATEGY: getting-started → quickstarts → mental model → verticals in build-order), the contract stated on the landing page (doc tag, index.md §How these docs are organized); remaining verticals ship overview+reference stubs per the v0.1 Tier-1 scope
445	[[requirements]]
446	id = "REQ-DOCS-4"
447	title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
448	required_stages = ["doc", "impl", "unit"]  # activated M6-D3 (schema leg): manifest_json_schema() generated from the SAME schemars derives that parse adapter manifests (drift impossible by construction), stable $id at the canonical Pages URL, checked-in crates/spt-runtime/manifest.schema.json = the site/release asset. llms.txt + CLI-ref legs land D5; MCP doc server + `--help --json` EXCLUDED → docs/DEFERRED.md (Tier 2, DOCS-STRATEGY §v0.1)
449	[[requirements]]
450	id = "REQ-DOCS-5"
451	title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
452	required_stages = ["impl", "int"]  # activated M6-D3 (schema gate): checked_in_schema_is_current — the checked-in asset must equal what the live derives generate, run in the normal CI sweep every push (regenerate via SPT_BLESS=1). CLI-ref + llms-export drift gates join at D5 through the same pattern
453
454	# ═════════════════════════════ KNOWN-HAZARDS invariants ══════════════════════
455	# Each maps to a docs/KNOWN-HAZARDS.md entry. On activation these require
456	# `unit` (and `int` where cross-process/cross-node) — the conformance checklist
457	# becomes a CI gate (PRD success criterion #6).
458
459	[[requirements]]
460	id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
461	title = "Grace-period wait completes before composing INIT_SIGNOFF (1.1)"
462	required_stages = ["impl", "unit"]
463	[[requirements]]
464	id = "REQ-HAZARD-INFO-JSON-TORN-READ"
465	title = "State-file reads tolerate concurrent writes (1.2)"
466	required_stages = ["impl", "unit"]  # activated M0: spt-store info.json atomic write + tolerant read (T12)
467	[[requirements]]
468	id = "REQ-HAZARD-STALE-INDEX-LOCK"
469	title = "Sweep stale lockfiles on daemon boot (1.3)"
470	required_stages = ["impl", "unit"]  # activated M5-D3e: branchstore::sweep_stale_index_locks (0-byte + age-gated, seed git-dir + worktree git-dirs) called from Daemon::run boot housekeeping
471	[[requirements]]
472	id = "REQ-HAZARD-DEFERRED-DRAIN"
473	title = "Deferred spool rows excluded from the event-stream drain (1.4)"
474	required_stages = ["impl", "unit"]  # activated M0: spt-store spool drain_non_deferred (T9)
475	[[requirements]]
476	id = "REQ-HAZARD-WORKER-PATH"
477	title = "Single source of truth for Worker/Psyche perch location (1.5)"
478	required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
479	[[requirements]]
480	id = "REQ-HAZARD-PARENT-PID-PREFER"
481	title = "Prefer stable parent PID / broker handle over ephemeral PID (2.1)"
482	required_stages = []  # rule 5: superseded in spirit by daemon-authoritative liveness (REQ-HAZARD-DAEMON-HOSTED-LIVENESS, int-covered M3b); binds if a per-pid path ever returns (none in the daemon model)
483	[[requirements]]
484	id = "REQ-HAZARD-STDIN-SESSION-ID"
485	title = "Stdin session_id precedence over env (2.2)"
486	required_stages = []  # rule 5: binds at the adapter api surface for harnesses that pass session ids on stdin — no such path exists in spt-core; re-assessed M5-D3e: the whole shell api surface passes identity as argv (--link token, positional shell-id), no stdin id seam grew through D3
487	[[requirements]]
488	id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
489	title = "Broker/brain IPC + handoff argv version-tolerant (2.3)"
490	required_stages = ["impl", "unit"]
491	[[requirements]]
492	id = "REQ-HAZARD-GEN-START-NOW"
493	title = "gen_start = now() on cold-start and handoff (2.4)"
494	required_stages = ["impl", "int"]
495	[[requirements]]
496	id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
497	title = "Ephemeral perch cleanup on every ring exit path (3.1)"
498	required_stages = ["impl", "unit"]  # activated M1 T4: ring cleans ephemeral perch on success/timeout/queued/error paths
499	[[requirements]]
500	id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
501	title = "Stale signoff sentinel does not kill a fresh start (3.2)"
502	required_stages = ["impl", "unit"]
503	[[requirements]]
504	id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
505	title = "Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)"
506	required_stages = ["impl", "unit"]
507	[[requirements]]
508	id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
509	title = "Envelope decode order, ampersand decoded last (4.1)"
510	required_stages = ["impl", "unit"]  # activated M0: spt-proto envelope codec
511	[[requirements]]
512	id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
513	title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
514	required_stages = ["impl", "unit"]  # activated M0: spt-proto two-slice parser
515	[[requirements]]
516	id = "REQ-HAZARD-EVENTPART-REASSEMBLY"
517	title = "EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently"
518	required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT-PART chunker (T3)
519	[[requirements]]
520	id = "REQ-HAZARD-ID-CHARSET"
521	title = "Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6)"
522	required_stages = ["impl", "unit"]  # activated 2026-06-02: spt-proto::id + 4 creation seams (forward-compat for ADR-0006 qualified addressing)
523	[[requirements]]
524	id = "REQ-HAZARD-REGISTRY-STALE-CLEAN"
525	title = "Stale registry entries degrade to fallback, never hard-fail (4.3)"
526	required_stages = ["impl", "unit"]  # activated M0: spt-store registry stale-clean (T11)
527	[[requirements]]
528	id = "REQ-HAZARD-REGISTRY-CONCURRENT"
529	title = "Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)"
530	required_stages = ["impl", "unit"]  # activated 2026-06-02: busy_timeout-before-WAL ordering (M1 store)
531	[[requirements]]
532	id = "REQ-HAZARD-REGISTRY-DIR-CREATE"
533	title = "SQLite store opens create their parent dir themselves — a fresh-home registry op must not SQLITE_CANTOPEN (4.9)"
534	required_stages = ["doc", "impl", "unit"]  # registered+activated 2026-06-03 (CI-bitten twice on hfenduleam): spt_store::registry::open_registry create_dir_all(owlery) before Connection::open; doc = KNOWN-HAZARDS 4.9; unit = register_on_nonexistent_owlery_creates_dir_and_succeeds
535	[[requirements]]
536	id = "REQ-HAZARD-REGISTRY-EPOCH-LEASE"
537	title = "Registry merge ordered by per-node monotonic epoch, never wall-clock — a stale Active can't clobber a newer Offline (4.8, red-team #8)"
538	required_stages = ["impl", "unit"]  # activated M4-D3b: spt-store::epoch::EpochSource (persisted strictly-increasing per-node counter, never wall-clock) + spt-net::net::registry::merge_instance version-vector lease keyed on (endpoint,node): strictly-greater-epoch wins, equal/lower dropped Stale. unit = stale_active_cannot_clobber_newer_offline + equal_epoch_replay_noop + epoch monotonic/persist/corrupt-safe. Cross-node replication of the merge = D4; chaos/two-host = D9.
539	[[requirements]]
540	id = "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN"
541	title = "Deferred rows survive poll drain (4.4)"
542	required_stages = ["impl", "unit"]  # activated M0: spt-store spool deferred rows (T9)
543	[[requirements]]
544	id = "REQ-HAZARD-INBOX-NO-DOUBLE"
545	title = "No double-delivery via legacy inbox (4.5)"
546	required_stages = ["impl", "unit"]  # activated M1 T2: delivery is TCP xor spool, never both
547	[[requirements]]
548	id = "REQ-HAZARD-WINDOWS-PID-RECYCLE"
549	title = "Windows PID-recycling false positives guarded (5.1)"
550	required_stages = ["impl", "unit"]  # activated M1 T2: connect-must-succeed guard + dead-pid row clean on delivery
551	[[requirements]]
552	id = "REQ-HAZARD-EBUSY-RENAME"
553	title = "tmp-write + atomic-rename + retry on Windows EBUSY (5.2)"
554	required_stages = ["impl", "unit"]  # activated M0: spt-store atomic write (T8)
555	[[requirements]]
556	id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
557	title = "Every harness/git subprocess has a timeout (5.3)"
558	required_stages = ["impl", "unit"]
559	[[requirements]]
560	id = "REQ-HAZARD-UNC-PATH-STRIP"
561	title = "Strip Windows UNC prefix on serialized paths (5.4)"
562	required_stages = ["impl", "unit"]  # activated M0: spt-store to_forward_slash (T8)
563	[[requirements]]
564	id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
565	title = "Single path/registry source of truth; no layout ambiguity (6.1)"
566	required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
567	[[requirements]]
568	id = "REQ-HAZARD-SOFT-CLEANUP"
569	title = "Soft-cleanup preserves state, removes only the ready marker (6.2)"
570	required_stages = ["impl", "unit"]  # activated M1 T3: soft_cleanup removes ready marker only, preserves spool + info.json
571	[[requirements]]
572	id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
573	title = "No hard-delete of a parent hosting non-empty children (6.3)"
574	required_stages = ["impl", "unit"]
575	[[requirements]]
576	id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
577	title = "Drop files are daemon-owned single-writer (6.4)"
578	required_stages = ["impl", "unit"]
579	[[requirements]]
580	id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
581	title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
582	required_stages = ["impl", "unit"]
583	[[requirements]]
584	id = "REQ-HAZARD-CONFLICT-BOTH-PRESERVED"
585	title = "A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)"
586	required_stages = ["impl", "unit"]  # activated M4-D6b: ContextStore::record_conflict (tracked .conflicts/ artifacts, SHA-256-prefix named — idempotent across nodes, replicate like context; local working file untouched) + list_conflicts + clear_conflicts (dominating-write-only). unit = both-versions-preserved + idempotent-recording + multi-version-artifacts + resolve-clears-exactly-this-file + clean-no-op
587	# --- Added 2026-05-31 from Stage A red-team + Spike #1 (inactive until M-activation) ---
588	[[requirements]]
589	id = "REQ-HAZARD-DETACHED-PIPE-INHERIT"
590	title = "Windows detached long-lived children must not inherit a captured caller's pipe: every detach-spawn of an immortal child (daemon, shell binary) runs bInheritHandles=FALSE, or a caller capturing output anywhere up the process chain hangs forever on a pipe that never EOFs — std-handle flag stripping is NOT sufficient (grandparent strays still flow) (5.6)"
591	required_stages = ["impl", "unit"]  # activated M5-D3e (paid twice within one slice: std-strip guard wedged on a grandparent pipe): daemon::detached_no_inherit shared by spawn_detached + launch_shell; quoting unit + the bounded-capture spawn in shell_e2e.rs as regression
592	[[requirements]]
593	id = "REQ-HAZARD-CONPTY-DSR"
594	title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
595	required_stages = ["impl", "unit"]
596	[[requirements]]
597	id = "REQ-HAZARD-CHILD-CONSOLE-FLASH"
598	title = "Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)"
599	required_stages = ["impl", "unit"]  # activated post-M7 field bug 2026-06-06: sync-pump git spawns flashed 2 windows/min on a Windows desktop; window-absence itself is untestable from a consoled test runner (child inherits the console) — the unit stage covers "flag does not break the spawn" (the error-87 regression class)
600	[[requirements]]
601	id = "REQ-HAZARD-INSTANT-UNDERFLOW"
602	title = "Scheduling never subtracts a Duration from Instant::now() (underflow-panics on a host booted more recently than the offset); 'due now / never run' is Option<Instant>=None gated on forward duration_since only (5.9)"
603	required_stages = ["impl", "unit"]  # activated 2026-06-07 CI failure: peer pump primed cadence legs with `Instant::now() - 86_400s`, panicking the pump thread on the sub-24h-uptime Windows runner (run 27082417706) so the subnet never converged. fix = peerloop::due(Option<Instant>) + None-seeded legs; unit asserts first-tick-due with zero instant subtraction (host-uptime-independent, the env-conditional E2E can't be the guard)
604	[[requirements]]
605	id = "REQ-HAZARD-SUDO-SECURE-PATH"
606	title = "Elevation guidance on Unix names the binary's ABSOLUTE path under sudo (a user-local install ~/.local/bin · ~/.cargo/bin is not on sudo's secure_path, so bare `sudo spt` dies 'command not found'); gated commands auto-elevate on an interactive TTY, else print the runnable hint (5.10)"
607	required_stages = ["impl", "unit"]  # activated 2026-06-07 field report (KITSUBITO): `spt subnet create` refused unelevated, user ran `sudo spt …` → `sudo: spt: command not found` (spt in ~/.local/bin, off secure_path). fix = elevation::rerun_command emits `sudo <abs-exe> <args>` (shell-quoted) + try_auto_elevate re-execs under sudo on interactive Unix; main.rs drops the confusing DEELEVATED line. unit = absolute-path-not-bare-name + shell-quoting + windows-none + should_auto_elevate truth table (the sudo exec itself needs a real TTY → manual/kitsubito verified)
608	[[requirements]]
609	id = "REQ-HAZARD-LOCAL-API-AUTH"
610	title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
611	required_stages = ["impl", "unit"]
612	[[requirements]]
613	id = "REQ-HAZARD-RESTART-IDEMPOTENT"
614	title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
615	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: tests/idempotent.rs crashes the brain before-intent/before-effect/after-effect at the PTY-write boundary → exactly-once (Spike #6)
616	[[requirements]]
617	id = "REQ-HAZARD-UPDATE-ROLLBACK"
618	title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
619	required_stages = ["impl", "unit"]  # activated M3c-C1: release::verify_metadata enforces monotonic version (rollback), metadata expiry, channel pinning, and key revocation. Adapter content signing is the C3 half (REQ-UPD-5/SEAM-UPDATE)
620	[[requirements]]
621	id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"
622	title = "Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)"
623	required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E spawns the hosted Psyche, then proves a dead summarizer pid does NOT flip the daemon-online perch offline
624	# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
625	[[requirements]]
626	id = "REQ-HAZARD-PSYCHE-OUTBOUND-PROXY"
627	title = "Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)"
628	required_stages = ["impl", "unit"]  # activated M4-D7.5: the stdout-captured turn driver (spt-live turn.rs, D6b pull-forward) + the intent parser's structural strip (spt-live outbound.rs) + the daemon relay's re-stamp/constrained-routing boundary (spt-daemon psyrelay.rs, anti-spoof negative test). int = D9 two-host
629	[[requirements]]
630	id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"
631	title = "Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)"
632	required_stages = ["impl", "unit"]  # activated M5-D3e on the hazard's shell-hosting face: every broker-hosted shell session gets its own drain + exit-waiter threads and every brain connection its own handler thread (broker.rs), proven by the hung-shell isolation test (one owner's hung shell binary stalls neither another owner's stdin delivery nor broker control calls). The daemon multi-agent PULSE fan-out face still pends (D4+/ADR-0004 consolidation): run_pulse_loop is def+test only and each agent drives its own pulse from its own process; M4-D9-2-3 note stands — the resting-edge feed points ARM per-endpoint echo gates (resting::arm_transition_echo) instead of hosting the bounded call, so that face binds at the per-agent-runtime fan-out
633	# --- Added 2026-06-03 at M4-D2 start (ADR-0005 Stage-A red-team #10/#11/#12); rule 3: register before satisfying, rule 5: activate per sub-task commit ---
634	[[requirements]]
635	id = "REQ-HAZARD-PAIR-TRANSCRIPT-BIND"
636	title = "Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)"
637	required_stages = ["impl", "unit"]  # activated M4-D2b: pairing::transcript HMAC-SHA256 confirmation MAC over the canonical transcript (domain/role/both-msgs/subnet/epoch/step/both-pubkeys, length-prefixed) + ct_eq verify, fed into the SPAKE2 KDF identity too. Negative tests in pairing::spake cover wrong-code, wrong-subnet, stale-step, stale-epoch, unknown-key-share, reflection (PAKE BadSide), tampered-tag. int = the on-wire MITM/replay E2E at D9
638	[[requirements]]
639	id = "REQ-HAZARD-PAIR-SEED-ROTATION"
640	title = "Removing a node rotates the subnet seed (epoch bump) so an old node/old seed cannot rejoin; trust-store delete alone is NOT revocation because the seed is replicated to every trusted node (ADR-0005 #10)"
641	required_stages = ["impl", "unit"]  # activated M4-D2c: spt-store::subnet SubnetStore holds N per-subnet TOTP seeds + monotonic epoch; rotate_seed mints fresh seed material + bumps the epoch (the node-removal response), persisted atomically + durable across reload. The epoch is bound into the pairing transcript (REQ-HAZARD-PAIR-TRANSCRIPT-BIND), and the spake stale_epoch/wrong_code tests prove an old seed/epoch can't rejoin. int = on-wire evict-then-rejoin E2E at D9
642	[[requirements]]
643	id = "REQ-HAZARD-PAIR-RATE-LIMIT"
644	title = "Subnet-global pairing rate limit: one active ceremony per subnet, shared attempt counter, exponential backoff — a public pre-trust relay + multiple seed-holders otherwise enables distributed SPAKE2 guessing (and ±1 TOTP window triples the valid-password space) (ADR-0005 #11)"
645	required_stages = ["impl", "unit"]  # activated M4-D2d: spt-net::pairing::ratelimit PairingRateLimiter — one in-flight ceremony per subnet (Busy), shared consecutive-failure counter, exponential backoff (1s→cap 3600s, abandoned ceremonies reclaimed+charged), per-subnet isolated. The ±1 TOTP window is JUSTIFIED (not dropped): serialization + exp backoff caps the guess rate so 3/1e6 success-per-guess stays out of reach inside a 30s code. int = on-wire distributed-guessing rejection at D2-wire/D9.
646	# --- Added 2026-06-03 at M4-D5a start (ADR-0009 origin-node gate; rule 3: register before satisfying) ---
647	[[requirements]]
648	id = "REQ-HAZARD-WAN-ORIGIN-AUTH"
649	title = "WAN-inbound origin is transport truth, never payload: the access gate's subject (ADR-0009 origin-node whitelist) is the QUIC handshake-proven remote node id from the broker's conn/stream table — a forged origin/node field inside record bytes is inert (7.5)"
650	required_stages = ["doc", "impl", "unit"]  # activated M4-D5a: WanMessage carries NO origin field by design (a forged one decodes as an ignored unknown field — spt-net wanmsg unit), and wan::receive_wan takes origin_node from NetStreamInfo.remote_id_hex (tests/wanmsg.rs proves the stream-table origin IS the handshake identity and a forged payload origin is inert end-to-end)
651
652	# --- Added 2026-06-04 at M5-D0 (M5-PLAN scope decisions; rule 3: register before satisfying, rule 5: activate per sub-task commit) ---
653	[[requirements]]
654	id = "REQ-CONSENT-1"
655	title = "Consent grant store: capability x subject-agent x target-node rows, enforced at the target node, subnet-settable (replicates as security material near the trust store), revocable; gated-capability ids (remote-exec, instantiate-anywhere) reserved-but-refusing; v1 consumers are the shell spawn gates (CONTEXT Consent & security gates)"
656	required_stages = ["impl", "unit"]  # activated M5-D1a: spt-store::grants GrantStore (trust/grants.json, default-deny polarity, corrupt-degrades-closed, exact-tuple match incl. qualifier) + spt-daemon::grants gate (reserved-refusal-first decide, target-node-local check front door) + spt grant add/revoke/list CLI (reserved ids refuse authoring). int = the D3d spawn-gate consumer E2E. M5-D9b rule-5: int stays unactivated — the escalation's end-to-end leg needs a real harness session answering a real prompt, which is the downstream plugin's acceptance territory (docs/DEFERRED.md)
657	[[requirements]]
658	id = "REQ-CONSENT-2"
659	title = "Interactive consent escalation: an ungated high-risk action routes a consent prompt to the user's most-recently-active session; allow-once / allow-always (writes a grant) / deny; pre-consent flags (can_shutdown, shell_wake_spawn_anywhere) author grants via manifest/settings (CONTEXT Consent & security gates)"
660	required_stages = ["impl", "unit"]  # activated M5-D1b: spt-daemon::grants escalation — EscalationAsk lossless body roundtrip over the consent-kind notif (produce_escalation_notif rides produce_and_first_fire to the most-recently-active session) + apply_escalation_answer (allow-once proceeds unpersisted / allow-always writes the exact tuple / deny persists nothing). D1c: the pre-consent flag authoring paths (author_can_shutdown_grant live-now; author_wake_spawn_anywhere_grant flag-shape-only — qualified shell-wake row the reserved gate still refuses until instantiate-anywhere lands). int = the D3d spawn-gate consumer E2E
661	[[requirements]]
662	id = "REQ-PRES-1"
663	title = "Presence resolution: the presence datum (last_active_node, last_active_endpoint, ts) gossiped subnet-wide via the agent-interaction heartbeat (rides registry distribution, visibility-gated) + one first-class most-recently-active resolution API consumed by notif first-fire, update-consent delivery, consent escalation, and shell wake resolution (M5 scope decision 1: resolution only — the PresenceChannel endpoint stays deferred)"
664	required_stages = ["impl", "unit", "int"]  # activated M5-D6a/b: the presence datum is an ADDITIVE FIELD on the registry Instance (last_active_ms, serde-default skip-if-none — the resources-blurb precedent verbatim), carried by advertise_local from the perch's heartbeat stamp (lifecycle::touch_active — Active seats only, a resting seat never wins recency). No new gossip channel: rides the epoch lease + RegistryUpdate replication, visibility-gated by construction. Wall-clock ms cross-compared ONLY as a routing heuristic — the epoch stays the sole merge-precedence key. unit = lease-ordering (recency never rolls back through a stale merge) + serde forward-compat + advertisement carry (stamped/never-stamped/refresh-epoch-bumped). D6b: spt-daemon::presence MRA API (local live perches ∪ visible routable gossiped rows under one max; PresenceTarget carries the local-perch handle iff the winner is this node); first_fire is the ONE swap point (update-consent + grants escalation both route through it as notif producers — their advisory targets re-resolve at fire time); RemoteTarget = skip-local-surface-unmarked, the row rides existing notif replication and the winner's node surfaces on feed-apply (dispatch::surface_fresh_rows — convergent decisions, no new wire op, seen-marks + suppression window dedup the skew race); resolve_wake's no-local-owner branch forwards the wake via the D5b rest op (WAKE_FORWARDED; cross-node shell link stays D8c). int = loopback two-broker E2E (remote-won notif redirects: produced at A unmarked, surfaced at B via the production dispatcher). Rig [twohost] leg waits for D9a.
665	[[requirements]]
666	id = "REQ-SHELL-1"
667	title = "Shell hosting machinery: shell perch under the owner (type/owner/adapter_name/status/alias), broker-launched binary + api bind local-link handshake, the three channels (command durable, text+file durable + progress-queryable, sensory REST-only never spooled + dropped-unless-owner-live), owner exclusivity (CONTEXT Shell model)"
668	required_stages = ["impl", "unit", "int"]  # activated M5-D3c; int activated M5-D9b: the D3e/D8 loopback E2Es + the rig cross-node shell drive rung (tests/twohost.rs, run 26998058816) carry the int tags
669	[[requirements]]
670	id = "REQ-SHELL-2"
671	title = "Shell sleep/wake: link-break always closes the binary (pre-close instruction + termination timeout), ephemeral teardown vs persistent offline/relink, wake_command wake-watcher (offline-only, exit-opcode supervision, exponential backoff + give-up), state-keyed wake resolution (dormant/suspended/active-elsewhere; no-reachable refuses — spawn-anywhere branch deferred), spt shutdown owner cascade + api owner-shutdown gated by can_shutdown (CONTEXT Shell sleep/wake)"
672	required_stages = ["impl", "unit", "int"]  # activated M5-D4a; impl/unit across D4a-D4d, int at the D4 sleep/wake E2E (shell_sleepwake_e2e.rs)
673
674	# --- Added 2026-06-06 from the M7 acceptance run's two publish-blocking field bugs (DEFERRED.md "NEXT SESSION" rows; rule 3: register before satisfying) ---
675	[[requirements]]
676	id = "REQ-HAZARD-ELEVATED-DAEMON-SPAWN"
677	title = "The daemon always runs unelevated in the invoking user's universe, regardless of which command spawns it: an elevated spawner de-elevates (Windows: UAC linked token via CreateProcessWithTokenW; Linux: drop to SUDO_UID/SUDO_GID + the invoker's HOME) — an elevated daemon's pipes deny unelevated clients (every later spt reads not-running→spawn→bind Access-denied) and a sudo'd daemon roots the user's state universe (5.7)"
678	required_stages = ["doc", "impl", "unit"]  # activated 2026-06-06 (M7 closeout fix 1): spt-daemon::deelevate (the OS-split de-elevation seam: windows linked-token respawn, unix sudo-invoker drop) consumed by daemon::spawn_detached + the Daemon::run entry guard. unit = the pure decision seams (sudo-invoker derivation from euid/env; the windows spawn-plan decision); the live elevated probe is environment-dependent (manual / acceptance rig)
679	[[requirements]]
680	id = "REQ-HAZARD-REGISTRY-GHOST-ROWS"
681	title = "A dead node identity's registry rows must decay: only the per-(endpoint,node) epoch lease supersedes rows, so without eviction a vanished node's rows are immortal and poison bare-id resolution with phantom AcrossNodes ambiguity — evict rows whose author node has not been heard (admitted inbound feed) within the eviction window; own rows never decay; a revived node re-inserts from its durable epoch within one pump cadence (4.10)"
682	required_stages = ["doc", "impl", "unit"]  # activated 2026-06-06 (M7 closeout fix 2): SubnetRegistry::evict_nodes (model) + RegistryHost::evict_silent_peers (heard-map TTL, snapshots rewritten) driven from the registry pump tick; registry_evict_after_ms knob (default 300s = 10 default cadences). unit = model eviction + the field-bug replay (ghost row makes bare resolve Ambiguous; eviction restores clean resolution) + self-rows-never-decay + recently-heard survives
683

required_stages = ["doc", "impl"]  # doc activated M6-D4 (Tier-1 corpus, agent audience first-class on the landing page); impl activated M6-D5: the second depth is machinery — xtask `site` publishes raw .md alongside every HTML page (/x.html ↔ /x.md), generates llms-full.txt from SUMMARY order, places llms.txt/schema/scripts at stable site-root URLs, ships rustdoc; docs-publish.yml is the tag-triggered pipeline to the spt-releases gh-pages branch (rendered snapshot; truth + history stay here, ADR-0014)
[[requirements]]
id = "REQ-DOCS-2"
title = "Sub-10-minute runnable killer quickstart per audience"
required_stages = ["doc", "int"]  # activated M6-D4: both killer quickstarts authored with REAL captured outputs, zero placeholders (messaging: install→two agents message incl. the QUEUED/backlog beat; adapter: walked via the shipped mock adapter, every command verified against the binary). int = quickstart_e2e.rs runs the published messaging flow step for step in the normal CI sweep — the doc cannot silently rot
[[requirements]]
id = "REQ-DOCS-3"
title = "Diátaxis structure; one canonical way to do X"
required_stages = ["doc"]  # activated M6-D4: the corpus IS the structure — per-vertical overview/tutorial/how-to/reference separation (SUMMARY.md IA per DOCS-STRATEGY: getting-started → quickstarts → mental model → verticals in build-order), the contract stated on the landing page (doc tag, index.md §How these docs are organized); remaining verticals ship overview+reference stubs per the v0.1 Tier-1 scope
[[requirements]]
id = "REQ-DOCS-4"
title = "Agent-consumable layer (llms.txt, manifest schema, MCP, CLI help)"
required_stages = ["doc", "impl", "unit"]  # activated M6-D3 (schema leg): manifest_json_schema() generated from the SAME schemars derives that parse adapter manifests (drift impossible by construction), stable $id at the canonical Pages URL, checked-in crates/spt-runtime/manifest.schema.json = the site/release asset. llms.txt + CLI-ref legs land D5; MCP doc server + `--help --json` EXCLUDED → docs/DEFERRED.md (Tier 2, DOCS-STRATEGY §v0.1)
[[requirements]]
id = "REQ-DOCS-5"
title = "Anti-drift: rustdoc/schema/exports/CLI-help generated + CI-checked"
required_stages = ["impl", "int"]  # activated M6-D3 (schema gate): checked_in_schema_is_current — the checked-in asset must equal what the live derives generate, run in the normal CI sweep every push (regenerate via SPT_BLESS=1). CLI-ref + llms-export drift gates join at D5 through the same pattern

# ═════════════════════════════ KNOWN-HAZARDS invariants ══════════════════════
# Each maps to a docs/KNOWN-HAZARDS.md entry. On activation these require
# `unit` (and `int` where cross-process/cross-node) — the conformance checklist
# becomes a CI gate (PRD success criterion #6).

[[requirements]]
id = "REQ-HAZARD-GRACE-BEFORE-SIGNOFF"
title = "Grace-period wait completes before composing INIT_SIGNOFF (1.1)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-INFO-JSON-TORN-READ"
title = "State-file reads tolerate concurrent writes (1.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-store info.json atomic write + tolerant read (T12)
[[requirements]]
id = "REQ-HAZARD-STALE-INDEX-LOCK"
title = "Sweep stale lockfiles on daemon boot (1.3)"
required_stages = ["impl", "unit"]  # activated M5-D3e: branchstore::sweep_stale_index_locks (0-byte + age-gated, seed git-dir + worktree git-dirs) called from Daemon::run boot housekeeping
[[requirements]]
id = "REQ-HAZARD-DEFERRED-DRAIN"
title = "Deferred spool rows excluded from the event-stream drain (1.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store spool drain_non_deferred (T9)
[[requirements]]
id = "REQ-HAZARD-WORKER-PATH"
title = "Single source of truth for Worker/Psyche perch location (1.5)"
required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
[[requirements]]
id = "REQ-HAZARD-PARENT-PID-PREFER"
title = "Prefer stable parent PID / broker handle over ephemeral PID (2.1)"
required_stages = []  # rule 5: superseded in spirit by daemon-authoritative liveness (REQ-HAZARD-DAEMON-HOSTED-LIVENESS, int-covered M3b); binds if a per-pid path ever returns (none in the daemon model)
[[requirements]]
id = "REQ-HAZARD-STDIN-SESSION-ID"
title = "Stdin session_id precedence over env (2.2)"
required_stages = []  # rule 5: binds at the adapter api surface for harnesses that pass session ids on stdin — no such path exists in spt-core; re-assessed M5-D3e: the whole shell api surface passes identity as argv (--link token, positional shell-id), no stdin id seam grew through D3
[[requirements]]
id = "REQ-HAZARD-HANDOFF-ARGV-COMPAT"
title = "Broker/brain IPC + handoff argv version-tolerant (2.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-GEN-START-NOW"
title = "gen_start = now() on cold-start and handoff (2.4)"
required_stages = ["impl", "int"]
[[requirements]]
id = "REQ-HAZARD-EPHEMERAL-CLEANUP"
title = "Ephemeral perch cleanup on every ring exit path (3.1)"
required_stages = ["impl", "unit"]  # activated M1 T4: ring cleans ephemeral perch on success/timeout/queued/error paths
[[requirements]]
id = "REQ-HAZARD-STALE-SIGNOFF-SENTINEL"
title = "Stale signoff sentinel does not kill a fresh start (3.2)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-ECHO-BEFORE-SIGNOFF"
title = "Echo-commune fires before INIT_SIGNOFF on orphan teardown (3.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-ENVELOPE-DECODE-ORDER"
title = "Envelope decode order, ampersand decoded last (4.1)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto envelope codec
[[requirements]]
id = "REQ-HAZARD-ENVELOPE-PARSER-SAFE"
title = "Two-slice envelope parser is panic-free and tolerant (4.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-proto two-slice parser
[[requirements]]
id = "REQ-HAZARD-EVENTPART-REASSEMBLY"
title = "EVENT-PART split/reassembly is byte-exact; orphan parts dropped silently"
required_stages = ["impl", "unit"]  # activated M0: spt-proto EVENT-PART chunker (T3)
[[requirements]]
id = "REQ-HAZARD-ID-CHARSET"
title = "Addressable-id charset reserves :/@ delimiters; validated at every creation seam (4.6)"
required_stages = ["impl", "unit"]  # activated 2026-06-02: spt-proto::id + 4 creation seams (forward-compat for ADR-0006 qualified addressing)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-STALE-CLEAN"
title = "Stale registry entries degrade to fallback, never hard-fail (4.3)"
required_stages = ["impl", "unit"]  # activated M0: spt-store registry stale-clean (T11)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-CONCURRENT"
title = "Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)"
required_stages = ["impl", "unit"]  # activated 2026-06-02: busy_timeout-before-WAL ordering (M1 store)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-DIR-CREATE"
title = "SQLite store opens create their parent dir themselves — a fresh-home registry op must not SQLITE_CANTOPEN (4.9)"
required_stages = ["doc", "impl", "unit"]  # registered+activated 2026-06-03 (CI-bitten twice on hfenduleam): spt_store::registry::open_registry create_dir_all(owlery) before Connection::open; doc = KNOWN-HAZARDS 4.9; unit = register_on_nonexistent_owlery_creates_dir_and_succeeds
[[requirements]]
id = "REQ-HAZARD-REGISTRY-EPOCH-LEASE"
title = "Registry merge ordered by per-node monotonic epoch, never wall-clock — a stale Active can't clobber a newer Offline (4.8, red-team #8)"
required_stages = ["impl", "unit"]  # activated M4-D3b: spt-store::epoch::EpochSource (persisted strictly-increasing per-node counter, never wall-clock) + spt-net::net::registry::merge_instance version-vector lease keyed on (endpoint,node): strictly-greater-epoch wins, equal/lower dropped Stale. unit = stale_active_cannot_clobber_newer_offline + equal_epoch_replay_noop + epoch monotonic/persist/corrupt-safe. Cross-node replication of the merge = D4; chaos/two-host = D9.
[[requirements]]
id = "REQ-HAZARD-DEFERRED-SURVIVE-DRAIN"
title = "Deferred rows survive poll drain (4.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store spool deferred rows (T9)
[[requirements]]
id = "REQ-HAZARD-INBOX-NO-DOUBLE"
title = "No double-delivery via legacy inbox (4.5)"
required_stages = ["impl", "unit"]  # activated M1 T2: delivery is TCP xor spool, never both
[[requirements]]
id = "REQ-HAZARD-WINDOWS-PID-RECYCLE"
title = "Windows PID-recycling false positives guarded (5.1)"
required_stages = ["impl", "unit"]  # activated M1 T2: connect-must-succeed guard + dead-pid row clean on delivery
[[requirements]]
id = "REQ-HAZARD-EBUSY-RENAME"
title = "tmp-write + atomic-rename + retry on Windows EBUSY (5.2)"
required_stages = ["impl", "unit"]  # activated M0: spt-store atomic write (T8)
[[requirements]]
id = "REQ-HAZARD-SUBPROCESS-TIMEOUT"
title = "Every harness/git subprocess has a timeout (5.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-UNC-PATH-STRIP"
title = "Strip Windows UNC prefix on serialized paths (5.4)"
required_stages = ["impl", "unit"]  # activated M0: spt-store to_forward_slash (T8)
[[requirements]]
id = "REQ-HAZARD-SINGLE-PATH-SOURCE"
title = "Single path/registry source of truth; no layout ambiguity (6.1)"
required_stages = ["impl", "unit"]  # activated M0: spt-store perch resolver (T10)
[[requirements]]
id = "REQ-HAZARD-SOFT-CLEANUP"
title = "Soft-cleanup preserves state, removes only the ready marker (6.2)"
required_stages = ["impl", "unit"]  # activated M1 T3: soft_cleanup removes ready marker only, preserves spool + info.json
[[requirements]]
id = "REQ-HAZARD-CASCADE-WIPE-GUARD"
title = "No hard-delete of a parent hosting non-empty children (6.3)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-DROP-FILE-SINGLE-WRITER"
title = "Drop files are daemon-owned single-writer (6.4)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-DIRECT-WRITE-PRECEDENCE"
title = "Direct-write precedence marker (with node id) guards stale overwrite (6.5)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-CONFLICT-BOTH-PRESERVED"
title = "A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)"
required_stages = ["impl", "unit"]  # activated M4-D6b: ContextStore::record_conflict (tracked .conflicts/ artifacts, SHA-256-prefix named — idempotent across nodes, replicate like context; local working file untouched) + list_conflicts + clear_conflicts (dominating-write-only). unit = both-versions-preserved + idempotent-recording + multi-version-artifacts + resolve-clears-exactly-this-file + clean-no-op
# --- Added 2026-05-31 from Stage A red-team + Spike #1 (inactive until M-activation) ---
[[requirements]]
id = "REQ-HAZARD-DETACHED-PIPE-INHERIT"
title = "Windows detached long-lived children must not inherit a captured caller's pipe: every detach-spawn of an immortal child (daemon, shell binary) runs bInheritHandles=FALSE, or a caller capturing output anywhere up the process chain hangs forever on a pipe that never EOFs — std-handle flag stripping is NOT sufficient (grandparent strays still flow) (5.6)"
required_stages = ["impl", "unit"]  # activated M5-D3e (paid twice within one slice: std-strip guard wedged on a grandparent pipe): daemon::detached_no_inherit shared by spawn_detached + launch_shell; quoting unit + the bounded-capture spawn in shell_e2e.rs as regression
[[requirements]]
id = "REQ-HAZARD-CONPTY-DSR"
title = "ConPTY reader must auto-answer DSR (ESC[6n) or all child output stalls (5.5)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-CHILD-CONSOLE-FLASH"
title = "Console-subsystem children of the console-less daemon spawn with CREATE_NO_WINDOW, or each spawn flashes a visible blank window on the user's desktop (5.8)"
required_stages = ["impl", "unit"]  # activated post-M7 field bug 2026-06-06: sync-pump git spawns flashed 2 windows/min on a Windows desktop; window-absence itself is untestable from a consoled test runner (child inherits the console) — the unit stage covers "flag does not break the spawn" (the error-87 regression class)
[[requirements]]
id = "REQ-HAZARD-INSTANT-UNDERFLOW"
title = "Scheduling never subtracts a Duration from Instant::now() (underflow-panics on a host booted more recently than the offset); 'due now / never run' is Option<Instant>=None gated on forward duration_since only (5.9)"
required_stages = ["impl", "unit"]  # activated 2026-06-07 CI failure: peer pump primed cadence legs with `Instant::now() - 86_400s`, panicking the pump thread on the sub-24h-uptime Windows runner (run 27082417706) so the subnet never converged. fix = peerloop::due(Option<Instant>) + None-seeded legs; unit asserts first-tick-due with zero instant subtraction (host-uptime-independent, the env-conditional E2E can't be the guard)
[[requirements]]
id = "REQ-HAZARD-SUDO-SECURE-PATH"
title = "Elevation guidance on Unix names the binary's ABSOLUTE path under sudo (a user-local install ~/.local/bin · ~/.cargo/bin is not on sudo's secure_path, so bare `sudo spt` dies 'command not found'); gated commands auto-elevate on an interactive TTY, else print the runnable hint (5.10)"
required_stages = ["impl", "unit"]  # activated 2026-06-07 field report (KITSUBITO): `spt subnet create` refused unelevated, user ran `sudo spt …` → `sudo: spt: command not found` (spt in ~/.local/bin, off secure_path). fix = elevation::rerun_command emits `sudo <abs-exe> <args>` (shell-quoted) + try_auto_elevate re-execs under sudo on interactive Unix; main.rs drops the confusing DEELEVATED line. unit = absolute-path-not-bare-name + shell-quoting + windows-none + should_auto_elevate truth table (the sudo exec itself needs a real TTY → manual/kitsubito verified)
[[requirements]]
id = "REQ-HAZARD-LOCAL-API-AUTH"
title = "Every local `api` mutation authenticated to an endpoint/session (codex #13)"
required_stages = ["impl", "unit"]
[[requirements]]
id = "REQ-HAZARD-RESTART-IDEMPOTENT"
title = "Idempotent/exactly-once delivery across brain restart at every broker boundary (codex #14)"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: tests/idempotent.rs crashes the brain before-intent/before-effect/after-effect at the PTY-write boundary → exactly-once (Spike #6)
[[requirements]]
id = "REQ-HAZARD-UPDATE-ROLLBACK"
title = "Self-update rejects version rollback; metadata expiry + adapter content signing (codex #5)"
required_stages = ["impl", "unit"]  # activated M3c-C1: release::verify_metadata enforces monotonic version (rollback), metadata expiry, channel pinning, and key revocation. Adapter content signing is the C3 half (REQ-UPD-5/SEAM-UPDATE)
[[requirements]]
id = "REQ-HAZARD-DAEMON-HOSTED-LIVENESS"
title = "Daemon-hosted perches (Psyche, spt-hosted Self) derive liveness from the daemon endpoint table + info.json status, never is_process_alive(info.pid) (2.5)"
required_stages = ["impl", "unit", "int"]  # int activated M3b-B9: the daemon E2E spawns the hosted Psyche, then proves a dead summarizer pid does NOT flip the daemon-online perch offline
# --- Added 2026-06-03 from grill-with-docs (ADR-0012, hazard 7.3); inactive until M4-D7.5 ---
[[requirements]]
id = "REQ-HAZARD-PSYCHE-OUTBOUND-PROXY"
title = "Psyche outbound captured + sanitized: the live-Psyche turn driver captures stdout (never Stdio::null), and the daemon strips/re-stamps Psyche-supplied from=/target and constrains routing (reply→__REPLY_TO__ sender, notify→own user/subnet) (7.3)"
required_stages = ["impl", "unit"]  # activated M4-D7.5: the stdout-captured turn driver (spt-live turn.rs, D6b pull-forward) + the intent parser's structural strip (spt-live outbound.rs) + the daemon relay's re-stamp/constrained-routing boundary (spt-daemon psyrelay.rs, anti-spoof negative test). int = D9 two-host
[[requirements]]
id = "REQ-HAZARD-DAEMON-SCHED-NONBLOCKING"
title = "Per-agent pulse/psyche/echo-commune scheduling must not serialize across agents: each agent's bounded LLM call (echo-commune summarizer, Psyche turn) runs off the shared scheduler so one slow/hung call cannot stall another agent's tick (7.4)"
required_stages = ["impl", "unit"]  # activated M5-D3e on the hazard's shell-hosting face: every broker-hosted shell session gets its own drain + exit-waiter threads and every brain connection its own handler thread (broker.rs), proven by the hung-shell isolation test (one owner's hung shell binary stalls neither another owner's stdin delivery nor broker control calls). The daemon multi-agent PULSE fan-out face still pends (D4+/ADR-0004 consolidation): run_pulse_loop is def+test only and each agent drives its own pulse from its own process; M4-D9-2-3 note stands — the resting-edge feed points ARM per-endpoint echo gates (resting::arm_transition_echo) instead of hosting the bounded call, so that face binds at the per-agent-runtime fan-out
# --- Added 2026-06-03 at M4-D2 start (ADR-0005 Stage-A red-team #10/#11/#12); rule 3: register before satisfying, rule 5: activate per sub-task commit ---
[[requirements]]
id = "REQ-HAZARD-PAIR-TRANSCRIPT-BIND"
title = "Pairing transcript binds roles, both node pubkeys, subnet ID, seed epoch, TOTP time-step, and confirmation MACs — or unknown-key-share/reflection/wrong-subnet/replay pairing remain possible (ADR-0005 #12)"
required_stages = ["impl", "unit"]  # activated M4-D2b: pairing::transcript HMAC-SHA256 confirmation MAC over the canonical transcript (domain/role/both-msgs/subnet/epoch/step/both-pubkeys, length-prefixed) + ct_eq verify, fed into the SPAKE2 KDF identity too. Negative tests in pairing::spake cover wrong-code, wrong-subnet, stale-step, stale-epoch, unknown-key-share, reflection (PAKE BadSide), tampered-tag. int = the on-wire MITM/replay E2E at D9
[[requirements]]
id = "REQ-HAZARD-PAIR-SEED-ROTATION"
title = "Removing a node rotates the subnet seed (epoch bump) so an old node/old seed cannot rejoin; trust-store delete alone is NOT revocation because the seed is replicated to every trusted node (ADR-0005 #10)"
required_stages = ["impl", "unit"]  # activated M4-D2c: spt-store::subnet SubnetStore holds N per-subnet TOTP seeds + monotonic epoch; rotate_seed mints fresh seed material + bumps the epoch (the node-removal response), persisted atomically + durable across reload. The epoch is bound into the pairing transcript (REQ-HAZARD-PAIR-TRANSCRIPT-BIND), and the spake stale_epoch/wrong_code tests prove an old seed/epoch can't rejoin. int = on-wire evict-then-rejoin E2E at D9
[[requirements]]
id = "REQ-HAZARD-PAIR-RATE-LIMIT"
title = "Subnet-global pairing rate limit: one active ceremony per subnet, shared attempt counter, exponential backoff — a public pre-trust relay + multiple seed-holders otherwise enables distributed SPAKE2 guessing (and ±1 TOTP window triples the valid-password space) (ADR-0005 #11)"
required_stages = ["impl", "unit"]  # activated M4-D2d: spt-net::pairing::ratelimit PairingRateLimiter — one in-flight ceremony per subnet (Busy), shared consecutive-failure counter, exponential backoff (1s→cap 3600s, abandoned ceremonies reclaimed+charged), per-subnet isolated. The ±1 TOTP window is JUSTIFIED (not dropped): serialization + exp backoff caps the guess rate so 3/1e6 success-per-guess stays out of reach inside a 30s code. int = on-wire distributed-guessing rejection at D2-wire/D9.
# --- Added 2026-06-03 at M4-D5a start (ADR-0009 origin-node gate; rule 3: register before satisfying) ---
[[requirements]]
id = "REQ-HAZARD-WAN-ORIGIN-AUTH"
title = "WAN-inbound origin is transport truth, never payload: the access gate's subject (ADR-0009 origin-node whitelist) is the QUIC handshake-proven remote node id from the broker's conn/stream table — a forged origin/node field inside record bytes is inert (7.5)"
required_stages = ["doc", "impl", "unit"]  # activated M4-D5a: WanMessage carries NO origin field by design (a forged one decodes as an ignored unknown field — spt-net wanmsg unit), and wan::receive_wan takes origin_node from NetStreamInfo.remote_id_hex (tests/wanmsg.rs proves the stream-table origin IS the handshake identity and a forged payload origin is inert end-to-end)

# --- Added 2026-06-04 at M5-D0 (M5-PLAN scope decisions; rule 3: register before satisfying, rule 5: activate per sub-task commit) ---
[[requirements]]
id = "REQ-CONSENT-1"
title = "Consent grant store: capability x subject-agent x target-node rows, enforced at the target node, subnet-settable (replicates as security material near the trust store), revocable; gated-capability ids (remote-exec, instantiate-anywhere) reserved-but-refusing; v1 consumers are the shell spawn gates (CONTEXT Consent & security gates)"
required_stages = ["impl", "unit"]  # activated M5-D1a: spt-store::grants GrantStore (trust/grants.json, default-deny polarity, corrupt-degrades-closed, exact-tuple match incl. qualifier) + spt-daemon::grants gate (reserved-refusal-first decide, target-node-local check front door) + spt grant add/revoke/list CLI (reserved ids refuse authoring). int = the D3d spawn-gate consumer E2E. M5-D9b rule-5: int stays unactivated — the escalation's end-to-end leg needs a real harness session answering a real prompt, which is the downstream plugin's acceptance territory (docs/DEFERRED.md)
[[requirements]]
id = "REQ-CONSENT-2"
title = "Interactive consent escalation: an ungated high-risk action routes a consent prompt to the user's most-recently-active session; allow-once / allow-always (writes a grant) / deny; pre-consent flags (can_shutdown, shell_wake_spawn_anywhere) author grants via manifest/settings (CONTEXT Consent & security gates)"
required_stages = ["impl", "unit"]  # activated M5-D1b: spt-daemon::grants escalation — EscalationAsk lossless body roundtrip over the consent-kind notif (produce_escalation_notif rides produce_and_first_fire to the most-recently-active session) + apply_escalation_answer (allow-once proceeds unpersisted / allow-always writes the exact tuple / deny persists nothing). D1c: the pre-consent flag authoring paths (author_can_shutdown_grant live-now; author_wake_spawn_anywhere_grant flag-shape-only — qualified shell-wake row the reserved gate still refuses until instantiate-anywhere lands). int = the D3d spawn-gate consumer E2E
[[requirements]]
id = "REQ-PRES-1"
title = "Presence resolution: the presence datum (last_active_node, last_active_endpoint, ts) gossiped subnet-wide via the agent-interaction heartbeat (rides registry distribution, visibility-gated) + one first-class most-recently-active resolution API consumed by notif first-fire, update-consent delivery, consent escalation, and shell wake resolution (M5 scope decision 1: resolution only — the PresenceChannel endpoint stays deferred)"
required_stages = ["impl", "unit", "int"]  # activated M5-D6a/b: the presence datum is an ADDITIVE FIELD on the registry Instance (last_active_ms, serde-default skip-if-none — the resources-blurb precedent verbatim), carried by advertise_local from the perch's heartbeat stamp (lifecycle::touch_active — Active seats only, a resting seat never wins recency). No new gossip channel: rides the epoch lease + RegistryUpdate replication, visibility-gated by construction. Wall-clock ms cross-compared ONLY as a routing heuristic — the epoch stays the sole merge-precedence key. unit = lease-ordering (recency never rolls back through a stale merge) + serde forward-compat + advertisement carry (stamped/never-stamped/refresh-epoch-bumped). D6b: spt-daemon::presence MRA API (local live perches ∪ visible routable gossiped rows under one max; PresenceTarget carries the local-perch handle iff the winner is this node); first_fire is the ONE swap point (update-consent + grants escalation both route through it as notif producers — their advisory targets re-resolve at fire time); RemoteTarget = skip-local-surface-unmarked, the row rides existing notif replication and the winner's node surfaces on feed-apply (dispatch::surface_fresh_rows — convergent decisions, no new wire op, seen-marks + suppression window dedup the skew race); resolve_wake's no-local-owner branch forwards the wake via the D5b rest op (WAKE_FORWARDED; cross-node shell link stays D8c). int = loopback two-broker E2E (remote-won notif redirects: produced at A unmarked, surfaced at B via the production dispatcher). Rig [twohost] leg waits for D9a.
[[requirements]]
id = "REQ-SHELL-1"
title = "Shell hosting machinery: shell perch under the owner (type/owner/adapter_name/status/alias), broker-launched binary + api bind local-link handshake, the three channels (command durable, text+file durable + progress-queryable, sensory REST-only never spooled + dropped-unless-owner-live), owner exclusivity (CONTEXT Shell model)"
required_stages = ["impl", "unit", "int"]  # activated M5-D3c; int activated M5-D9b: the D3e/D8 loopback E2Es + the rig cross-node shell drive rung (tests/twohost.rs, run 26998058816) carry the int tags
[[requirements]]
id = "REQ-SHELL-2"
title = "Shell sleep/wake: link-break always closes the binary (pre-close instruction + termination timeout), ephemeral teardown vs persistent offline/relink, wake_command wake-watcher (offline-only, exit-opcode supervision, exponential backoff + give-up), state-keyed wake resolution (dormant/suspended/active-elsewhere; no-reachable refuses — spawn-anywhere branch deferred), spt shutdown owner cascade + api owner-shutdown gated by can_shutdown (CONTEXT Shell sleep/wake)"
required_stages = ["impl", "unit", "int"]  # activated M5-D4a; impl/unit across D4a-D4d, int at the D4 sleep/wake E2E (shell_sleepwake_e2e.rs)

# --- Added 2026-06-06 from the M7 acceptance run's two publish-blocking field bugs (DEFERRED.md "NEXT SESSION" rows; rule 3: register before satisfying) ---
[[requirements]]
id = "REQ-HAZARD-ELEVATED-DAEMON-SPAWN"
title = "The daemon always runs unelevated in the invoking user's universe, regardless of which command spawns it: an elevated spawner de-elevates (Windows: UAC linked token via CreateProcessWithTokenW; Linux: drop to SUDO_UID/SUDO_GID + the invoker's HOME) — an elevated daemon's pipes deny unelevated clients (every later spt reads not-running→spawn→bind Access-denied) and a sudo'd daemon roots the user's state universe (5.7)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-06 (M7 closeout fix 1): spt-daemon::deelevate (the OS-split de-elevation seam: windows linked-token respawn, unix sudo-invoker drop) consumed by daemon::spawn_detached + the Daemon::run entry guard. unit = the pure decision seams (sudo-invoker derivation from euid/env; the windows spawn-plan decision); the live elevated probe is environment-dependent (manual / acceptance rig)
[[requirements]]
id = "REQ-HAZARD-REGISTRY-GHOST-ROWS"
title = "A dead node identity's registry rows must decay: only the per-(endpoint,node) epoch lease supersedes rows, so without eviction a vanished node's rows are immortal and poison bare-id resolution with phantom AcrossNodes ambiguity — evict rows whose author node has not been heard (admitted inbound feed) within the eviction window; own rows never decay; a revived node re-inserts from its durable epoch within one pump cadence (4.10)"
required_stages = ["doc", "impl", "unit"]  # activated 2026-06-06 (M7 closeout fix 2): SubnetRegistry::evict_nodes (model) + RegistryHost::evict_silent_peers (heard-map TTL, snapshots rewritten) driven from the registry pump tick; registry_evict_after_ms knob (default 300s = 10 default cadences). unit = model eviction + the field-bug replay (ghost row makes bare resolve Ambiguous; eviction restores clean resolution) + self-rows-never-decay + recently-heard survives


Edit

required_stages = ["doc", "impl", "unit"]  # activated 2026-06-06 (M7 closeout fix 2): SubnetRegistry::evict_nodes (model) + RegistryHost::evict_silent_peers (heard-map TTL, snapshots rewritten) driven from the registry pump tick; registry_evict_after_ms knob (default 300s = 10 default cadences). unit = model eviction + the field-bug replay (ghost row makes bare resolve Ambiguous; eviction restores clean resolution) + self-rows-never-decay + recently-heard survives

required_stages = ["doc", "impl", "unit"]  # activated 2026-06-06 (M7 closeout fix 2): SubnetRegistry::evict_nodes (model) + RegistryHost::evict_silent_peers (heard-map TTL, snapshots rewritten) driven from the registry pump tick; registry_evict_after_ms knob (default 300s = 10 default cadences). unit = model eviction + the field-bug replay (ghost row makes bare resolve Ambiguous; eviction restores clean resolution) + self-rows-never-decay + recently-heard survives

# --- Added 2026-06-07 at M8 ratification (M8-PLAN.md decisions 1-24; rule 3:
# register before satisfying, rule 5: inactive until each deliverable starts.
# Planned activations: REQ-CLI-* at D1, REQ-SUBNET-5..8 at D2, REQ-INSTALL-6..8
# at D3, REQ-CONV-*/REQ-PAIR-8/REQ-DAEMON-5 at D4 (D4 also adds REQ-UPD-6 int
# per decision 19), REQ-HAZARD-EPOCH-RESET stays inactive by decision 24.) ---
[[requirements]]
id = "REQ-CLI-1"
title = "spt endpoint noun namespace: absorbs fork/suspend/wake/shutdown/rename/stop/digest + access (ported 1:1: allow|revoke|open|list, decision 21) + description (ex-resources blurb; bare=show, set=author); merged endpoint list [--local|--subnet <name>] grouped by subnet with SELF pinned; bare spt endpoint = the list (M8 decisions 1-2)"
required_stages = []
[[requirements]]
id = "REQ-CLI-2"
title = "spt daemon noun: run|stop|status (hidden daemon verb becomes daemon run; agent-endpoint shutdown keeps its name under endpoint); daemon status renders the pump heartbeat (last-tick recency) so a half-dead daemon is never rendered implied-healthy (M8 decisions 5, 23)"
required_stages = []
[[requirements]]
id = "REQ-CLI-3"
title = "Agent hot path stays flat across the M8 reorg: send/ring/ready/whoami/how-to unchanged; notify moves to subnet notify while notif stays top-level; breaking renames land clean with no deprecation shims (zero external CLI consumers pre-spt-claude-code) (M8 decisions 3-4, 9)"
required_stages = []
[[requirements]]
id = "REQ-SUBNET-5"
title = "Per-subnet serve-state: spt subnet detach <NAME> [--auto] / attach <NAME> [--auto] — daemon keeps running, stops/starts advertising + connecting for that subnet (peer pump + responder selective); --auto persists the startup default in daemon config; the all-attached banner gains per-subnet states (M8 decision 6)"
required_stages = []
[[requirements]]
id = "REQ-SUBNET-6"
title = "Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7)"
required_stages = []
[[requirements]]
id = "REQ-SUBNET-7"
title = "Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22)"
required_stages = []
[[requirements]]
id = "REQ-SUBNET-8"
title = "Status render honesty: zero-subnet text is daemon-aware ('No subnets registered — this node is standalone.' + daemon-running-dependent blurb, never implying messaging works while the daemon is down); hint footer prints on bare spt subnet only (status drops it); a stalled pump is surfaced in subnet status, never rendered implied-healthy (M8 decisions 11-12, 23)"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-6"
title = "Linux elevation install leg: install.sh symlinks the binary into a sudo-reachable path (/usr/local/bin; graceful print-the-one-liner when unelevated) so sudo spt resolves; first sudo spt detects elevation and prompts ONCE for the default user account — thereafter any elevated daemon launch runs daemon + state under that account, never root (KH 5.7 interplay verified) (M8 decision 8)"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-7"
title = "Windows inbound reachability: the elevated install leg registers the inbound-UDP firewall rule (New-NetFirewallRule); the daemon self-detects blocked inbound and renders it as the no-connection state in subnet status + the coming-online banner (covers user-scope installs that skip the elevated leg — never a silent NO_SEED_HOLDER dead-end) (M8 root cause 3)"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-8"
title = "OS-service registration (REQ-INSTALL-1's deferred third leg): Linux systemd USER service + loginctl enable-linger (linger rides the elevated install leg; daemon starts at boot pre-login, user universe per KH 5.7, systemctl --user managed); Windows scheduled task at-logon (interactive session, no stored credentials); a node is reachable after reboot without any manual spt invocation (M8 decision 17)"
required_stages = []
[[requirements]]
id = "REQ-CONV-1"
title = "Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)"
required_stages = []
[[requirements]]
id = "REQ-CONV-2"
title = "Event-driven advertisement: endpoint online/offline transitions (ready-listener start/stop, rest-state transition, perch death) trigger an immediate advertise_local + peer push as a WAKE of the existing pump loop (no second advertisement path — epoch lease + visibility gates ride unchanged); the cadence stays the steady-state floor (M8 decision 15)"
required_stages = []
[[requirements]]
id = "REQ-PAIR-8"
title = "NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)"
required_stages = []
[[requirements]]
id = "REQ-DAEMON-5"
title = "Pump liveness: the peer pump writes a last-tick heartbeat consumed by daemon status / subnet status (decision 23 render legs in REQ-CLI-2/REQ-SUBNET-8); the daemon supervises the pump task — a panic is caught, logged loudly, and the pump restarts with capped backoff (≤5 min), so a 5.9-class death self-heals visibly instead of silently halving the daemon (M8 decision 23; field motivation: hfenduleam 2026-06-07 half-death)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-EPOCH-RESET"
title = "Advertisement-epoch reset strands a node: peers' higher last-seen epoch drops the reset node's fresh advertisements as Stale until the counter outruns history. Common case (full reinstall/re-pair) is mitigated by REQ-SUBNET-7's ceremony eviction (peer-side epoch memory dies with the deleted row — acceptance-verified); the residual narrow slice (epoch file lost, identity kept) is documented, guard deferred to a field hit (4.11)"
required_stages = []  # rule 5 + M8 decision 24: documented hazard (KNOWN-HAZARDS 4.11 at D5), guard waits for a field hit of the narrow slice; the re-pair eviction path is tested under REQ-SUBNET-7


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

required_stages = ["doc", "impl", "unit"]  # activated 2026-06-06 (M7 closeout fix 2): SubnetRegistry::evict_nodes (model) + RegistryHost::evict_silent_peers (heard-map TTL, snapshots rewritten) driven from the registry pump tick; registry_evict_after_ms knob (default 300s = 10 default cadences). unit = model eviction + the field-bug replay (ghost row makes bare resolve Ambiguous; eviction restores clean resolution) + self-rows-never-decay + recently-heard survives

# --- Added 2026-06-07 at M8 ratification (M8-PLAN.md decisions 1-24; rule 3:
# register before satisfying, rule 5: inactive until each deliverable starts.
# Planned activations: REQ-CLI-* at D1, REQ-SUBNET-5..8 at D2, REQ-INSTALL-6..8
# at D3, REQ-CONV-*/REQ-PAIR-8/REQ-DAEMON-5 at D4 (D4 also adds REQ-UPD-6 int
# per decision 19), REQ-HAZARD-EPOCH-RESET stays inactive by decision 24.) ---
[[requirements]]
id = "REQ-CLI-1"
title = "spt endpoint noun namespace: absorbs fork/suspend/wake/shutdown/rename/stop/digest + access (ported 1:1: allow|revoke|open|list, decision 21) + description (ex-resources blurb; bare=show, set=author); merged endpoint list [--local|--subnet <name>] grouped by subnet with SELF pinned; bare spt endpoint = the list (M8 decisions 1-2)"
required_stages = []
[[requirements]]
id = "REQ-CLI-2"
title = "spt daemon noun: run|stop|status (hidden daemon verb becomes daemon run; agent-endpoint shutdown keeps its name under endpoint); daemon status renders the pump heartbeat (last-tick recency) so a half-dead daemon is never rendered implied-healthy (M8 decisions 5, 23)"
required_stages = []
[[requirements]]
id = "REQ-CLI-3"
title = "Agent hot path stays flat across the M8 reorg: send/ring/ready/whoami/how-to unchanged; notify moves to subnet notify while notif stays top-level; breaking renames land clean with no deprecation shims (zero external CLI consumers pre-spt-claude-code) (M8 decisions 3-4, 9)"
required_stages = []
[[requirements]]
id = "REQ-SUBNET-5"
title = "Per-subnet serve-state: spt subnet detach <NAME> [--auto] / attach <NAME> [--auto] — daemon keeps running, stops/starts advertising + connecting for that subnet (peer pump + responder selective); --auto persists the startup default in daemon config; the all-attached banner gains per-subnet states (M8 decision 6)"
required_stages = []
[[requirements]]
id = "REQ-SUBNET-6"
title = "Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7)"
required_stages = []
[[requirements]]
id = "REQ-SUBNET-7"
title = "Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22)"
required_stages = []
[[requirements]]
id = "REQ-SUBNET-8"
title = "Status render honesty: zero-subnet text is daemon-aware ('No subnets registered — this node is standalone.' + daemon-running-dependent blurb, never implying messaging works while the daemon is down); hint footer prints on bare spt subnet only (status drops it); a stalled pump is surfaced in subnet status, never rendered implied-healthy (M8 decisions 11-12, 23)"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-6"
title = "Linux elevation install leg: install.sh symlinks the binary into a sudo-reachable path (/usr/local/bin; graceful print-the-one-liner when unelevated) so sudo spt resolves; first sudo spt detects elevation and prompts ONCE for the default user account — thereafter any elevated daemon launch runs daemon + state under that account, never root (KH 5.7 interplay verified) (M8 decision 8)"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-7"
title = "Windows inbound reachability: the elevated install leg registers the inbound-UDP firewall rule (New-NetFirewallRule); the daemon self-detects blocked inbound and renders it as the no-connection state in subnet status + the coming-online banner (covers user-scope installs that skip the elevated leg — never a silent NO_SEED_HOLDER dead-end) (M8 root cause 3)"
required_stages = []
[[requirements]]
id = "REQ-INSTALL-8"
title = "OS-service registration (REQ-INSTALL-1's deferred third leg): Linux systemd USER service + loginctl enable-linger (linger rides the elevated install leg; daemon starts at boot pre-login, user universe per KH 5.7, systemctl --user managed); Windows scheduled task at-logon (interactive session, no stored credentials); a node is reachable after reboot without any manual spt invocation (M8 decision 17)"
required_stages = []
[[requirements]]
id = "REQ-CONV-1"
title = "Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)"
required_stages = []
[[requirements]]
id = "REQ-CONV-2"
title = "Event-driven advertisement: endpoint online/offline transitions (ready-listener start/stop, rest-state transition, perch death) trigger an immediate advertise_local + peer push as a WAKE of the existing pump loop (no second advertisement path — epoch lease + visibility gates ride unchanged); the cadence stays the steady-state floor (M8 decision 15)"
required_stages = []
[[requirements]]
id = "REQ-PAIR-8"
title = "NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)"
required_stages = []
[[requirements]]
id = "REQ-DAEMON-5"
title = "Pump liveness: the peer pump writes a last-tick heartbeat consumed by daemon status / subnet status (decision 23 render legs in REQ-CLI-2/REQ-SUBNET-8); the daemon supervises the pump task — a panic is caught, logged loudly, and the pump restarts with capped backoff (≤5 min), so a 5.9-class death self-heals visibly instead of silently halving the daemon (M8 decision 23; field motivation: hfenduleam 2026-06-07 half-death)"
required_stages = []
[[requirements]]
id = "REQ-HAZARD-EPOCH-RESET"
title = "Advertisement-epoch reset strands a node: peers' higher last-seen epoch drops the reset node's fresh advertisements as Stale until the counter outruns history. Common case (full reinstall/re-pair) is mitigated by REQ-SUBNET-7's ceremony eviction (peer-side epoch memory dies with the deleted row — acceptance-verified); the residual narrow slice (epoch file lost, identity kept) is documented, guard deferred to a field hit (4.11)"
required_stages = []  # rule 5 + M8 decision 24: documented hazard (KNOWN-HAZARDS 4.11 at D5), guard waits for a field hit of the narrow slice; the re-pair eviction path is tested under REQ-SUBNET-7

title = "A dead node identity's registry rows must decay: only the per-(endpoint,node) epoch lease supersedes rows, so without eviction a vanished node's rows are immortal and poison bare-id resolution with phantom AcrossNodes ambiguity — evict rows whose author node has not been heard (admitted inbound feed) within the eviction window; own rows never decay; a revived node re-inserts from its durable epoch within one pump cadence (4.10)"

+# --- Added 2026-06-07 at M8 ratification (M8-PLAN.md decisions 1-24; rule 3:

+# register before satisfying, rule 5: inactive until each deliverable starts.

+# Planned activations: REQ-CLI-* at D1, REQ-SUBNET-5..8 at D2, REQ-INSTALL-6..8

+# at D3, REQ-CONV-*/REQ-PAIR-8/REQ-DAEMON-5 at D4 (D4 also adds REQ-UPD-6 int

+# per decision 19), REQ-HAZARD-EPOCH-RESET stays inactive by decision 24.) ---

+title = "spt endpoint noun namespace: absorbs fork/suspend/wake/shutdown/rename/stop/digest + access (ported 1:1: allow|revoke|open|list, decision 21) + description (ex-resources blurb; bare=show, set=author); merged endpoint list [--local|--subnet <name>] grouped by subnet with SELF pinned; bare spt endpoint = the list (M8 decisions 1-2)"

+title = "spt daemon noun: run|stop|status (hidden daemon verb becomes daemon run; agent-endpoint shutdown keeps its name under endpoint); daemon status renders the pump heartbeat (last-tick recency) so a half-dead daemon is never rendered implied-healthy (M8 decisions 5, 23)"

+title = "Agent hot path stays flat across the M8 reorg: send/ring/ready/whoami/how-to unchanged; notify moves to subnet notify while notif stays top-level; breaking renames land clean with no deprecation shims (zero external CLI consumers pre-spt-claude-code) (M8 decisions 3-4, 9)"

+title = "Per-subnet serve-state: spt subnet detach <NAME> [--auto] / attach <NAME> [--auto] — daemon keeps running, stops/starts advertising + connecting for that subnet (peer pump + responder selective); --auto persists the startup default in daemon config; the all-attached banner gains per-subnet states (M8 decision 6)"

+title = "Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7)"

+title = "Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22)"

+title = "Status render honesty: zero-subnet text is daemon-aware ('No subnets registered — this node is standalone.' + daemon-running-dependent blurb, never implying messaging works while the daemon is down); hint footer prints on bare spt subnet only (status drops it); a stalled pump is surfaced in subnet status, never rendered implied-healthy (M8 decisions 11-12, 23)"

+title = "Linux elevation install leg: install.sh symlinks the binary into a sudo-reachable path (/usr/local/bin; graceful print-the-one-liner when unelevated) so sudo spt resolves; first sudo spt detects elevation and prompts ONCE for the default user account — thereafter any elevated daemon launch runs daemon + state under that account, never root (KH 5.7 interplay verified) (M8 decision 8)"

+title = "Windows inbound reachability: the elevated install leg registers the inbound-UDP firewall rule (New-NetFirewallRule); the daemon self-detects blocked inbound and renders it as the no-connection state in subnet status + the coming-online banner (covers user-scope installs that skip the elevated leg — never a silent NO_SEED_HOLDER dead-end) (M8 root cause 3)"

+title = "OS-service registration (REQ-INSTALL-1's deferred third leg): Linux systemd USER service + loginctl enable-linger (linger rides the elevated install leg; daemon starts at boot pre-login, user universe per KH 5.7, systemctl --user managed); Windows scheduled task at-logon (interactive session, no stored credentials); a node is reachable after reboot without any manual spt invocation (M8 decision 17)"

+title = "Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)"

+title = "Event-driven advertisement: endpoint online/offline transitions (ready-listener start/stop, rest-state transition, perch death) trigger an immediate advertise_local + peer push as a WAKE of the existing pump loop (no second advertisement path — epoch lease + visibility gates ride unchanged); the cadence stays the steady-state floor (M8 decision 15)"

+title = "NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)"

+title = "Pump liveness: the peer pump writes a last-tick heartbeat consumed by daemon status / subnet status (decision 23 render legs in REQ-CLI-2/REQ-SUBNET-8); the daemon supervises the pump task — a panic is caught, logged loudly, and the pump restarts with capped backoff (≤5 min), so a 5.9-class death self-heals visibly instead of silently halving the daemon (M8 decision 23; field motivation: hfenduleam 2026-06-07 half-death)"

+title = "Advertisement-epoch reset strands a node: peers' higher last-seen epoch drops the reset node's fresh advertisements as Stale until the counter outruns history. Common case (full reinstall/re-pair) is mitigated by REQ-SUBNET-7's ceremony eviction (peer-side epoch memory dies with the deleted row — acceptance-verified); the residual narrow slice (epoch file lost, identity kept) is documented, guard deferred to a field hit (4.11)"

+required_stages = []  # rule 5 + M8 decision 24: documented hazard (KNOWN-HAZARDS 4.11 at D5), guard waits for a field hit of the narrow slice; the re-pair eviction path is tested under REQ-SUBNET-7


PowerShell

traceable-reqs check; echo "exit=$LASTEXITCODE"

Validate requirements registry