Scope: the design contract for Phase 13 (WLA-13-01). One substrate — a state feed, a correlation model, an event log, and a consent envelope — consumed by every mission-control client: the HoldSpeak Desk conveyor (counterpart phase, their repo), the Telegram interface (WLA-13-06), and whatever surface comes after. Stories WLA-13-02 through 13-06 implement against this document; when reality disagrees, the story amends this document in the same commit. Claim marks as in riders.md: verified-live (run on this machine, date recorded), cited (file pinned), decided (a choice this document makes and owns).
Verification date: 2026-07-04.
Phase 12 left more on the table than the scaffold specs assumed:
- HoldSpeak's agent-session registry is richer than "cwd and a
flag." Verified-live at
~/.config/holdspeak/agent_sessions.json: each record carriesagent,session_id,model,repo_root(already resolved by their hook — no cwd-walking needed),repo_anchor,project_name,tmux_session/tmux_window/tmux_pane/tmux_pane_current_path(the driver's addressing, already solved),awaiting_response,last_assistant_text,last_prompt,last_tool_name,transcript_path,created_at/updated_at. The codex session from the WLA-12-05 proof is in it. - The feed's source of truth exists:
dw context --compactalready computes projects, phases, stories, statuses, evidence trace, and next actionable story (dw_pmo.api). - The write path exists: the Phase 12 actuator plugin and
gated connector (two
dw storyverbs, allow-listed argv, payload-hash parity), with the dw gate keeping final say — crown-proven in WLA-12-03's evidence. - tmux 3.6b is installed (verified-live);
send-keysandcapture-paneare the driver and preview primitives.
The feed is one of several read surfaces; the whole inventory (CLI, workbench HTTP, MCP) is contracted in interop.md.
Decided: the feed is a CLI invocation, dw state --json, not
a served endpoint — the cheapest thing a consumer can poll, no
daemon, no port, works in every repo the rails are installed in.
dw state --json --write <path> additionally drops the same
document to a file for consumers that prefer watching one.
The feed is a versioned, stable subset of the context payload —
dw context --compact remains the CLI-facing view and may change
shape; the feed may not, without a version bump:
{
"feed_schema": 1,
"generated_at_tree": "<git index tree at render>",
"projects": [{
"slug": "…", "prefix": "…",
"current_phase": {"number": 12, "title": "…", "status": "…"},
"next_story": {"story_id": "…", "title": "…", "status": "…"} ,
"phases": [{"number": 12, "title": "…", "status": "open|closed",
"stories_done": 5, "stories_total": 8}],
"stories": [{"story_id": "…", "title": "…", "status": "…",
"phase": 12, "evidence_exists": true}],
"warnings": 2
}],
"orchestration_runs": {
"kind": "delivery-workbench-run-summary-list",
"schema_version": 1,
"runs": [{
"run_id": "run-…", "state": "active", "score": "research-build-review",
"project": "…", "story": "…", "active_claims": 2,
"completed_claims": 0, "ledger_events": 5, "ledger_head": "sha256:…",
"expired": false, "expires_at": "…", "budgets": {}
}],
"starts_work": false,
"writes_events": false
}
}(Amended by WLA-13-02 in its own commit, per this document's
rule: a per-project phases array joined the schema before
freezing — the Desk conveyor renders phases as the belt, and the
actuator pack validates create-targets against phases that may
hold no stories yet; neither works from current_phase alone.
current_phase uses the same phase shape.)
Phase 24 adds the stamped, content-safe orchestration_runs summary under
the repository-wide additive-versioning rule in interop.md. It contains
only ids, states, counts, hashes, expiry, and budgets—never score prompts,
provider/check argv, credentials, transcripts, source, or artifact content.
The run ledger remains separate from the rail event journal.
Schema-pinning tests fail on unannounced shape changes. Consumers
declare the feed_schema they were proven against, the way the
HoldSpeak pack MANIFEST declares its version range.
Decided: the correlator reads HoldSpeak's registry file read-only (no API dependency, no writes, ever) and joins on the field their hook already resolves:
- For each registry record:
repo_rootnames a directory that is a rails repo (haspm/roadmap/and.githooks/dw) → join to that repo's in-progress stories from the feed. - Exactly one in-progress story →
on_story. More than one →ambiguous, all listed (unknown beats guessed). None →idle_on_rails.repo_rootnot a rails repo →off_rails. (Amended by WLA-13-03:) rails markers present but the roadmap unparseable →unreadable, a fifth outcome implementation surfaced — a repo we cannot read is not "off the rails," and guessing either way would lie. - A record whose
updated_atis older than a staleness TTL (decided: 30 minutes) is reportedstale: true, never dropped silently. awaiting_responseandlast_assistant_textride the correlation verbatim — they are the Q&A relay payload the Telegram interface forwards.
The registry is desk-runtime state on a 0.x project: the correlator reads every field defensively with the observed field list pinned in its tests, and a shape change is a documented compatibility note, not a silent break — the pack precedent.
(Amended by WLA-13-03, verified live:) the registry file is
{"version": 1, "sessions": {"<agent>:<session_id>": {record}}} —
it carries its own version field, and the correlator refuses
politely on any version it was not proven against. And correlation
is its own document (dw sessions --json, sessions_schema
1), not a feed key: the feed is per-repo and frozen; sessions span
every repo on the desk and carry desk-runtime state. §5 already
listed them as separate consumables; clients merge them.
(Amended by WLA-13-06:) dw sessions grew --registry <path> —
the correlator always took the path as a parameter; the CLI now
exposes it, so fixture tests and any desk with a nonstandard
registry location can point at a file without touching dw
internals. The default remains the HoldSpeak desk registry.
Decided: an append-only JSONL file at .git/pmo-events.jsonl
— beside the contract archive, surviving aborted commits, never
itself committed (events are local telemetry about the repo, not
repo content). One line per event:
{"ts": "…Z", "event": "gate_refusal", "project": "…",
"story": "…", "detail": {"rule": "…"}, "tree": "<index tree>"}Taxonomy v1 — exactly the moments the machinery already observes:
story_status (with from/to), evidence_capture (with exit
code), gate_pass, gate_refusal (with rule id),
contract_generated, phase_created, phase_closed. Grown in
v1.1 (WLA-17-02) with the pause/resume moments: phase_paused,
phase_resumed (each with phase).
Grown in v1.2 (WLA-23-02) with step_execution: exactly one event for each
child that crosses the deliberate-step boundary, carrying only action id,
outcome, exit code, before/after token hashes, and the newly observed action
id. Preview, refusal, and process-start failure do not emit it. The underlying
guarded command may also emit its existing domain event; the step event is the
transport-neutral execution correlation, never a replacement.
The consent stance, binding: events carry rails metadata only —
story IDs, statuses, rule ids, exit codes, tree hashes. Never
diff content, never transcript or prompt text, never file paths
outside pm/roadmap. A content-audit test enforces this. The
event log answers "what happened on the rails," not "what did the
human type."
Three rings, strictest last:
- Read (state, events, correlation,
capture-panepreviews): owner-only, bound by pairing rather than hardcoded identity (amended by owner decision, 2026-07-04): the interface generates a one-time, short-TTL pairing token visible only on the operator's machine; the owner supplies it in chat; the binding lives in the interface's runtime state (chmod 600, outside the repo), is revoked by re-pairing, and no chat or user ID is ever configuration we author. Previews are verbatim and read-only. (Amended by WLA-20-01/02, owner direction 2026-07-11:) the read ring gained pictures —/screenrenders the pane to a PNG (ANSI colors honored, in-place 🔄 refresh) and/liveserves an auto-refreshing photo behind the same content-hash gate the text view always had. Rendering is an optional capability (Pillow): absent, both fall back to text with the reason stated. Still read-only, still never a keystroke. (Amended by WLA-20-03, owner direction 2026-07-11 — groups:) pairing now records WHO redeemed the token (from.id) as the owner-of-record. In a chat where other humans exist, the consent-bearing commands, every callback tap, and the steering relay answer only to that identity — refusal by name; reads stay chat-scoped. This transmutes the refused user-ID allowlist rather than repealing it: no allowlist exists anywhere; the pairing act itself names the owner, and re-pairing renames it. A state paired before this field existed keeps chat-granularity behavior and/statussays so. An update carrying no sender identity against a recorded owner is refused — unproven identity is not identity. - Rails verbs (story flips, story/project creation): every
act is proposal → preview → in-chat approval, executed through
the Phase 12 actuator/connector seam where it exists; project
creation is additionally path-allow-listed to
workspace_rootsdeclared in the operator config (~/.config/delivery-workbench/telegram.json), lands only as rails-installed + doctor-green + first gated commit, and is refused outside the roots. (Amended by WLA-13-06, the bootstrap-certification decision:) the first gated commit of a freshly created project needs a certified contract, and certification is human, always. Here the human act is the owner's approval tap on a preview that names exactly what will be certified; the interface records that consent in the contract (--consent yes --reasons) and flips the boxes of the bootstrap contract only — a commit in a repo with no stories, no evidence, and no history, every rule mechanically checkable, the dw gate re-verifying each stamped fact downstream. Story-work certification is never delegated this way, and the two allow-listed story verbs cannot commit at all. Pairing tokens (ring 1) are decided at 5-minute TTL, single-use, stored only as a sha256 at rest. - The tmux driver — the sharpest edge, named honestly: once a
tmux session is armed,
send-keysrelays free text, and no allow-list can bound free text into a terminal. Therefore the arming is the consent boundary, and it is engineered, not promised: per-session explicit grant from the owner, default TTL 15 minutes, auto-expiry, visible at any time via a status command, revocable in one message, everything off by default. An unarmed session refuses (test-proven). The registry'stmux_session/tmux_panefields give precise addressing — the driver targets the agent's own pane, never "whatever is focused." The dw gate below remains the last word regardless of who typed. (Amended by owner decision, 2026-07-04, from the live phone leg:) the approval tap on a reply proposal doubles as the arming grant when the session is not yet armed — the preview names the arming explicitly, the default TTL and the/armed-visibility and one-message revocation are unchanged, and the driver-level refusal below the chat layer stands. Two taps collapsed into one explicit one; the boundary did not move. (Amended by WLA-14-01, owner decision 2026-07-04 — the interaction stance:) consent gates ENTRY, not every utterance. Binding a session into a topic IS the arming — one explicit, visible, revocable act with an activity-refreshed TTL — and inside a live binding, conversation flows: typed text relays directly, replies land back, no per-message proposal (13-06's per-reply proposal is retired). Taps remain at the boundaries the gate cares about: rails verbs, project lifecycle, session launch/recovery. Pane-ownership verification per keystroke and the dw gate's final say are unchanged. Full map: absorption-ccgram.md §0. (Amended by WLA-20-04/05, owner direction 2026-07-11 — the button surfaces:) inside a live binding the buttons grew up: the toolbar is per-harness configuration (key/text/builtin actions; the builtin table is CLOSED — a config can rearrange and relabel, never mint capability), and a pushed question from a bound AND armed session carries a nav keyboard (arrows, Enter, Esc, 📸) that drives the actual TUI prompt. Every key/text action still enters the terminal through the one driver door with its per-keystroke ownership check; a nav tap NEVER arms (eligibility is the two grants that already exist); and every tap is owner-checked upstream. The slash menu is registered with Telegram (setMyCommands, read-and-entry verbs only, config opt-out).
The Desk conveyor (HoldSpeak repo) and any other client consume
exactly three things: the feed (§1), the event log (§3), and the
correlation output (§2). No private scraping of pm/roadmap, no
reading dw internals. Each client declares its proven
feed_schema and the registry field list it was tested against.
Drift between a client and the substrate is a compatibility note
on the client, not a silent break.
(Amended by the counterpart scaffold, 2026-07-04:) the
HoldSpeak-side phase is now specced against this section — their
repo, pm/roadmap/holdspeak/phase-82-mission-control-conveyor/
(branch holdspeak/hs-82-mission-control-conveyor, opened 0/5):
a FastAPI bridge relays the three documents to the Desk island,
the belt renders them, and the approval leg rides §4 ring 2
through the Phase 12 connector. Their HS-82-05 carries the
desk-side legs that WLA-13-05's exit exam cites.
Decided: the journal continues into Phase 13 under the same charter (docs/journal/README.md) — same voice, same cadence, same honesty bar. The worked example gets richer, entry 10 onward.
- WLA-13-02 implements §1 exactly: the verb is
dw state --json(+--write), the schema above is the contract,dw contextstays independent, one real consumer converts. - WLA-13-03 implements §2: registry file read-only, the four correlation outcomes, the 30-minute staleness TTL, tests pinning the observed field list.
- WLA-13-04 implements §3's original seven-event taxonomy, the
.git/pmo-events.jsonllocation, the content-audit test. - WLA-13-05 consumes §1+§3 from the Desk; its approval leg rides ring 2 of §4.
- WLA-13-06 implements §4 in full; its tmux addressing comes
from the registry fields verified above; the bot process lives
in this repo under
integrations/telegram/(decided — the HoldSpeak relay seam can join later without moving it).