Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ The operator's real TODO, organized (2026-07-22): six numbered items, each groun

- [ ] **roadmap-driven dispatch — machinery landed, go-live proven** — gunbc serve on srv1 serves the live roadmap, and POST /dispatch/NODE_ID invokes belt_dispatch_node in-process. The belt observes fail-closed over tmux, reconciles the ready frontier, and spawns a worktree plus a tmux claude session carrying the filled dispatch brief, with typed refusals at every arm. Go-live was proven by execution on srv1: spawn, idempotent re-POST, and a bogus-id refusal, each independently read back. — ⏳ awaiting sign-off
- [ ] **re-dispatch after Stop is broken — the dispatch loop is one-shot per node** — Two failure classes, both proven live on srv1. Debris: Stop leaves the worktree and `dispatch/<node_id>` branch behind, so the next POST is a typed spawn_failed at the git step. Session-id reuse: even after hand-cleaning, claude exits 1 because the deterministic per-node UUID collides with the archived transcript — one-shot at the claude layer too.
- [ ] **make ownership a recorded fact, not an inferred one** — Ownership is INFERRED at observation: the belt reads the pane's process name and judges whether the session is its own. That cannot be made total — the tmux namespace is host-global, so any process running as that user may wear a dispatch name — and it has an open edge (a real claude reports its version string as pane_current_command on macOS). Record ownership at MINT instead: spawn writes the attempt identity plus the pane's (pid, start-time), which no unprivileged process can forge; observation reads it back. Foreign becomes absent-from-the-ledger, not a fingerprint judgement.
- [ ] **UI modeling migration — kill the hand layer; unlock the component register; land /sandbox** — Five phases on #7096's wire vocabulary: P0 the component concept (DispatchButton states DERIVED from the wire authority, totality cross-checked both ways) + the scale token axes; P1 string-JS → ONE modeled TsProgram, emitted as a served asset; P2 every px/font through scale vars, proven byte-neutral by execution; P3 presentation from the model; P4 /sandbox at every depth + gallery + live echo arms; P5 the dead belt-A emit DELETED. Plus the D1–D5 anemia dissolution, same PR: MediaType per RFC 9110 · typed HttpStatus · CssLength · the exemplar roster enrolled · the CSS property/selector grain — pure re-grounding, bytes unchanged.
- [ ] **until the actuator lands, this sheet is the intake queue** — new work enters as a row here or as a sized lane node with an Accept line, never ad hoc mid-session; dispatch top-down by the numbering; a session that discovers work files a row instead of chasing it.
- [ ] **each dispatched item is a real workflow, not a single hop** — Stages: MODEL first (concept/authority delta + acceptance claims), BRIEF generated from row + model with the bar as checkable claims, IMPLEMENT, ANTAGONISTIC REVIEW against the brief — never the PR's self-description — LAND with the verdict updating this sheet. The wall: MERGE IS NOT DONE — a merge certifies the diff is safe; only a review verdict moves status. The MODEL stage is unskippable and first, to the ed25519 citation bar (every referenced upstream entity carries its own authority); a brief without the model stage's output is not dispatchable.
Expand All @@ -99,6 +100,7 @@ The operator's real TODO, organized (2026-07-22): six numbered items, each groun
- [ ] **the DAG progress bar as pure projection** — Verdict-owned done bits, Volume-weighted, rolled up the existing RoadmapEdge fan-in; per-subtree completion with fan-out/fan-in visible. No stored completion percentage anywhere — a derived read on the same tree the belt walks, rendered via the register/component machinery.
- [ ] **the typed stage chain, status derived** — ModelArtifact, then Brief generated from row plus model, then Attempt, then Verdict — typed on the carrier. The verdict is the only status mover: done derives from an Accepted verdict, and backing up to an earlier stage is a re-derivation because everything downstream is generated from the model artifact.
- [ ] **dispatch-time walls in belt_dispatch_node** — Refuse dispatch on a node without a ModelArtifact; refuse on stale receipts (the freshness check from ts-wf-node-schema); a NeedsRework verdict dispatches a FRESH session carrying brief + prior diff + located debts — never the producing context unwinding its own work. Typed refusal arms on the existing BeltDispatchResult vocabulary, so the wire and UI pick them up for free through the component register.
- [ ] **cross-session messaging — sessions that hand off, not just finish** — Dispatched sessions are mutually blind: each gets a brief at spawn and answers only by landing a branch. The lane needs more — a NeedsRework verdict must reach a fresh session; a session that discovers work must file a row; the page should show what a worker says while it is saying it. The carrier is the durable state the stage chain already needs, read a second way: append-only, typed, attempt-keyed. Not a chat channel — a ledger both sides read, so a message is a FACT with an author and a time.
- [ ] **floor lenses over the roadmap value** — Pure readers (the v2 read interface): done-has-verdict; AcceptedWithResidue-has-counted-rows; dispatched-has-model-artifact.

**3a · The stabilization loop — why item 3 exists (receipts):**
Expand Down
8 changes: 8 additions & 0 deletions dag/extdeps/tmux/tmux.dag
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ fn shape_tmux_kill_session_argv(session_name: String) -> List<String> {
["kill-session", "-t", session_name]
}

data tmux_list_panes_format_note: String = "The pane-fingerprint read (upstream: tmux FORMATS, man tmux — session_name and pane_current_command are cited format variables; pane_current_command is the name of the process running IN the pane, which is exactly the ownership evidence the dispatch lease classification needs). -a lists panes across all sessions in one read. Single-space separator: tmux session names in this system are single tokens (gunbc-dispatch-<node_id>) and pane_current_command is a process name — the parse splits on the FIRST space, so even a hypothetical spaced command survives as the fingerprint's head token, which is the token the classification compares."

data tmux_list_panes_format: String = "#\{session_name\} #\{pane_current_command\}"

fn shape_tmux_list_panes_argv() -> List<String> {
["list-panes", "-a", "-F", tmux_list_panes_format]
}

service tmux.Session {
operation New {
input { session_name: String, working_dir: String, inner_argv: List<String> }
Expand Down
39 changes: 36 additions & 3 deletions dag/gunbc/roadmap_authority.dag
Original file line number Diff line number Diff line change
Expand Up @@ -2322,16 +2322,17 @@ fn task_sheet_lead_lane() -> RoadmapSection {
headline: "re-dispatch after Stop is broken — the dispatch loop is one-shot per node",
brief: "Two failure classes, both proven live on srv1. Debris: Stop leaves the worktree and `dispatch/<node_id>` branch behind, so the next POST is a typed spawn_failed at the git step. Session-id reuse: even after hand-cleaning, claude exits 1 because the deterministic per-node UUID collides with the archived transcript — one-shot at the claude layer too.",
displaced_cost: "a stopped node becomes dispatchable again without a human cleaning worktrees and branches on srv1",
current_state: "found live 2026-07-23; both classes reproduce by execution; no fix landed",
current_state: "fix on PR #7266 (U1), green by execution; both classes dissolved at the source rather than cleaned up after — awaiting the operator verdict, so NOT done",
first_slice: "teardown/spawn own the worktree+branch lifecycle observe-then-decide (the session_lease upsert shape); a worktree with uncommitted work REFUSES removal, never silently deletes",
red_control: "re-POST after Stop must not answer spawn_failed; a worktree with uncommitted changes must refuse teardown",
out_of_scope: "the sessions panel (PR-B P2a) — this row is only the re-dispatch loop",
handback: "the resume-vs-fresh choice: `--resume <uuid>` continues the node's prior work (arguably the right semantic) vs a per-attempt UUID — operator picks; either way a typed observation arm, never a fabricated fresh-spawn",
handback: "the resume-vs-fresh choice is TAKEN as fresh-per-attempt (see the 2026-07-25 update) and stays the operator's to overturn — reversing it is a one-line derivation change, not a redesign",
updates: [
TicketUpdate { on: "2026-07-23", note: "both failure classes reproduced live on srv1; dedup shown independent of UUID determinism (observe keys on the tmux session name)" },
TicketUpdate { on: "2026-07-25", note: "U1 (#7266): dispatch became ATTEMPT-grain — branch, worktree, and claude session-id all derive from content_hash(node_id x clock probed_at), so the debris class cannot collide and the archived-transcript class cannot recur. The tmux session name stays NODE-grain because it is the observe/reconcile key: fresh where debris collides, stable where identity reconciles. The handback is resolved fresh-per-attempt, not --resume: a resumed session inherits the prior attempt's context, which is exactly what ts-wf-belt-refusals rules out for NeedsRework (a fresh session carrying brief + prior diff, never the producing context unwinding its own work) — the two rows would otherwise disagree about the same question" },
],
authored_on: "2026-07-23",
last_verified_on: "2026-07-23",
last_verified_on: "2026-07-25",
}, intricacy: IntricacyMedium, volume: VolumeSmall, repo: "gunbc"),
ticket_row(id: "ts-ui-model", done: false, t: TicketFields {
headline: "UI modeling migration — kill the hand layer; unlock the component register; land /sandbox",
Expand Down Expand Up @@ -2397,10 +2398,26 @@ fn task_sheet_lead_lane() -> RoadmapSection {
authored_on: "",
last_verified_on: "",
}),
ticket_wi(id: "ts-dispatch-ownership-mint", done: false, t: TicketFields {
headline: "make ownership a recorded fact, not an inferred one",
brief: "Ownership is INFERRED at observation: the belt reads the pane's process name and judges whether the session is its own. That cannot be made total — the tmux namespace is host-global, so any process running as that user may wear a dispatch name — and it has an open edge (a real claude reports its version string as pane_current_command on macOS). Record ownership at MINT instead: spawn writes the attempt identity plus the pane's (pid, start-time), which no unprivileged process can forge; observation reads it back. Foreign becomes absent-from-the-ledger, not a fingerprint judgement.",
displaced_cost: "the fingerprint's unclosable edge — the belt stops guessing whether a session is its own, so the green kill path needs no per-host process-name receipt",
current_state: "U4 (#7266) landed the fingerprint gate: every refusal arm proven live, but the KILL arm compares a process name whose value is host-dependent (the 2.1.220 finding)",
first_slice: "the spawn ledger — what the belt minted, keyed by attempt. This is the same durable state ts-wf-stage-artifacts needs, so the two land on ONE carrier or neither",
red_control: "a hand-made session wearing a dispatch name classifies Foreign with NO fingerprint read at all; a ledger entry whose (pid, start-time) no longer matches classifies Displaced, never Converged",
out_of_scope: "forbidding the name collision itself — the tmux session namespace is not ours to close, and calling that a wall would be the DESIGN section-5 'never' trap",
handback: "whether the belt should own a PRIVATE tmux server (its own socket) instead of sharing the user's — that genuinely narrows the squat surface, but it changes host provisioning, so the operator owns it",
updates: [
TicketUpdate { on: "2026-07-25", note: "operator question at the U4 stop-button receipt: 'my interest would be to make it impossible for a foreign process to exist here'. Recorded rather than answered by fingerprint tuning, because the honest reading is that the NAME is not ours to own — what can move is the PROOF: ownership recorded at mint is decidable and forgery-resistant where a name comparison is neither" },
],
authored_on: "2026-07-25",
last_verified_on: "",
}, intricacy: IntricacyHigh, volume: VolumeMedium, repo: "gunbc"),
],
edges: [
RoadmapEdge { child: nid(s: "ts-dispatch-verdict"), parent: nid(s: "ts-dispatch-lifecycle") },
RoadmapEdge { child: nid(s: "ts-dispatch-rework"), parent: nid(s: "ts-dispatch-lifecycle") },
RoadmapEdge { child: nid(s: "ts-dispatch-ownership-mint"), parent: nid(s: "ts-dispatch-redispatch") },
],
),
section_group(
Expand Down Expand Up @@ -2553,11 +2570,27 @@ fn task_sheet_lead_lane() -> RoadmapSection {
authored_on: "2026-07-22",
last_verified_on: "",
}, intricacy: IntricacyLow, volume: VolumeSmall, repo: "gunbc"),
ticket_wi(id: "ts-wf-messaging", done: false, t: TicketFields {
headline: "cross-session messaging — sessions that hand off, not just finish",
brief: "Dispatched sessions are mutually blind: each gets a brief at spawn and answers only by landing a branch. The lane needs more — a NeedsRework verdict must reach a fresh session; a session that discovers work must file a row; the page should show what a worker says while it is saying it. The carrier is the durable state the stage chain already needs, read a second way: append-only, typed, attempt-keyed. Not a chat channel — a ledger both sides read, so a message is a FACT with an author and a time.",
displaced_cost: "the operator relaying between sessions by hand — copying a verdict into the next brief, watching a pane to learn what a worker is doing",
current_state: "nothing recorded before this row; TicketUpdate append is the named first API (ticket_write_interface_note) and the U2 sessions panel is the first reader that would render it",
first_slice: "one direction only — worker to ledger to page: a session appends typed TicketUpdates to its own node, /sessions.json carries them, the row renders them. The reverse direction (ledger to a RUNNING worker) waits for a proven reader, because a write nobody reads is the inert-lens lie",
red_control: "an append from a session whose attempt is not the node's current attempt REFUSES — a stale worker cannot write over a newer attempt's record",
out_of_scope: "agent-to-agent chat, and any channel letting a worker mutate the roadmap outside the updates axis",
handback: "whether a worker may file NEW rows (intake) or only append to its own — ts-intake-discipline says work enters through the sheet, and a writing worker is the first thing that could bypass it",
updates: [
TicketUpdate { on: "2026-07-25", note: "operator question at the U4 receipt: 'i think we'll also need cross session comms/messaging?'. Recorded as a lane row rather than built, because its carrier is the SAME durable state ts-wf-stage-artifacts and ts-dispatch-ownership-mint need — building a messaging channel on its own storage would fork that state three ways before it exists once" },
],
authored_on: "2026-07-25",
last_verified_on: "",
}, intricacy: IntricacyHigh, volume: VolumeMedium, repo: "gunbc"),
],
edges: [
RoadmapEdge { child: nid(s: "ts-wf-belt-refusals"), parent: nid(s: "ts-wf-stage-artifacts") },
RoadmapEdge { child: nid(s: "ts-wf-belt-refusals"), parent: nid(s: "ts-dispatch-redispatch") },
RoadmapEdge { child: nid(s: "ts-wf-progress"), parent: nid(s: "ts-wf-node-schema") },
RoadmapEdge { child: nid(s: "ts-wf-messaging"), parent: nid(s: "ts-wf-stage-artifacts") },
],
),
section_group(
Expand Down
Loading
Loading