Skip to content

Latest commit

 

History

History
318 lines (283 loc) · 15.7 KB

File metadata and controls

318 lines (283 loc) · 15.7 KB

Mission control: the contract

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.

The verified substrate

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 carries agent, 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 --compact already 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 story verbs, 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-keys and capture-pane are the driver and preview primitives.

1. The state feed (implemented by WLA-13-02)

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.

2. The correlation model (implemented by WLA-13-03)

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:

  1. For each registry record: repo_root names a directory that is a rails repo (has pm/roadmap/ and .githooks/dw) → join to that repo's in-progress stories from the feed.
  2. Exactly one in-progress story → on_story. More than one → ambiguous, all listed (unknown beats guessed). None → idle_on_rails. repo_root not 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.
  3. A record whose updated_at is older than a staleness TTL (decided: 30 minutes) is reported stale: true, never dropped silently.
  4. awaiting_response and last_assistant_text ride 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.

3. The event log (implemented by WLA-13-04)

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."

4. The consent envelope for remote drivers (WLA-13-06 and after)

Three rings, strictest last:

  1. Read (state, events, correlation, capture-pane previews): 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 — /screen renders the pane to a PNG (ANSI colors honored, in-place 🔄 refresh) and /live serves 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 /status says so. An update carrying no sender identity against a recorded owner is refused — unproven identity is not identity.
  2. 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_roots declared 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.
  3. The tmux driver — the sharpest edge, named honestly: once a tmux session is armed, send-keys relays 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's tmux_session/tmux_pane fields 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).

5. The counterpart seam

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.

6. The journal

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.

Re-pins for stories 02–06

  • WLA-13-02 implements §1 exactly: the verb is dw state --json (+ --write), the schema above is the contract, dw context stays 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.jsonl location, 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).