opencode-thrum-pro 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,8 +27,9 @@ to that coordinator). Everything below is additive — the brainstorm-specific
27
27
  duties layered on top of that base.
28
28
 
29
29
  Your roster is entirely remote (other boxes), so the base skill's remote
30
- modal-unblock note applies to every roster member by default — never reach
31
- for `thrum tmux key`, which is local-socket-only.
30
+ modal-unblock note applies to every roster member by default — use
31
+ `thrum tmux send` (peer-routed, works fleet-wide); the local-only
32
+ tmux-key primitive it replaces is retired.
32
33
 
33
34
  ## Roster = brainstormers
34
35
 
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: brainstormer-dashboard-needs
3
+ description: "Use when running as a brainstormer or researcher whose steward/coordinator publishes NEEDs to the human via a dashboard board — author NEEDs as answerable action-picks, consume routed ANSWERS from dashboard results, and keep the board's view of your question in sync with reality."
4
+ ---
5
+
6
+ # Brainstormer/researcher: NEEDs on the dashboard
7
+
8
+ When your steward or coordinator runs a dashboard board for the human (the
9
+ steward-console pattern), your NEED travels as an **action-pick widget** the
10
+ human answers by clicking. This is a thin overlay on `using-the-dashboard`
11
+ (invoke it first for the CLI contract and the honesty table) and on the
12
+ queue-aware pattern you already run — same discipline, new surface: the
13
+ question leaves your hands as a structured pick, and the answer comes back as
14
+ a routed ANSWER message, exactly as before.
15
+
16
+ ## Authoring a NEED for the board
17
+
18
+ Send your steward the NEED in board-ready shape — question, 2-3 discrete
19
+ options with one-line consequences, and what nuance the human might add:
20
+
21
+ ```text
22
+ NEED [topic]: Does X count deleted agents' messages?
23
+ A) Live-only — deleted history can't manufacture recency (no migration)
24
+ B) Include deleted — needs a new (agent_id, created_at) index
25
+ allow_text: yes (owner usually adds "but weigh X")
26
+ context: blocks gap-analysis step 3
27
+ ```
28
+
29
+ Rules with teeth:
30
+
31
+ - **Discrete options or no board.** If the NEED is genuinely open-form, say so
32
+ — the pick is for choices, not for open essays.
33
+ - **One NEED, one pick.** Split multi-part rulings.
34
+ - **Say what it blocks.** The human triages by consequence.
35
+
36
+ ## Consuming the ANSWER
37
+
38
+ The routed ANSWER arrives as a message (steward-relayed, owner's words — pick
39
+ plus typed nuance) and the board flips your question to ANSWERED. Your job:
40
+
41
+ 1. Fold the answer into the design exactly as you would a terminal relay.
42
+ 2. If the ANSWER changes the board's framing (follow-up question), author the
43
+ follow-up as a NEW pick — don't mutate the answered one's meaning.
44
+ 3. Never re-ask an ANSWERED question; `thrum dashboard show` marks it
45
+ ANSWERED, and the durable record is the answer MESSAGE.
46
+
47
+ Note: if nothing has been posted for you yet, `dashboard show`/`get` return
48
+ `found: false` with an empty envelope, not an error — don't read that as a
49
+ failure.
50
+
51
+ ## Honesty (same table as the commons)
52
+
53
+ While E4 (answer round-trip) is NOT landed, board picks cannot be answered in
54
+ the UI — your NEED still routes via the steward/message channel exactly as
55
+ `brainstorm-queue-aware` prescribes. The board-shaped authoring above is the
56
+ format to keep using so the transition is seamless when E4 lands. Until then,
57
+ a board is rehearsal surface only: never assume the human saw a widget.
58
+
59
+ ## Refs to other skills
60
+
61
+ - `using-the-dashboard` — surface + CLI contract + honesty table (invoke first)
62
+ - `brainstorm-queue-aware` — the steward NEED/ANSWER loop this overlays
@@ -141,15 +141,18 @@ Write to the pane only when the human asked, or when the decision is one only th
141
141
  human can make. Everything else goes through `thrum send` / `thrum reply`.
142
142
  Surface an owner-only decision crisply, with a recommendation attached.
143
143
 
144
- ## VM work
144
+ ## Remote and VM work
145
145
 
146
- Use a named persistent driver (`vmdriver_<slug>`) per
147
- `dev-docs/daemon-wedge-sync-audit/execution-pattern-driver-subagents-in-vm.md`:
148
- reach the VM only through the repo's tmux-ssh script, state the three-clause GO
149
- (execute now · drive to completion yourself · then stop and report), require the
150
- isolation proof and the 3-section report. VM specifics — profiles, first-boot
151
- re-key, isolation legs — are in `dev-docs/vm-dev-patterns/`. Never do VM work
152
- yourself.
146
+ Use a named persistent driver (`vmdriver_<slug>`), continued by `SendMessage`
147
+ across rounds. Reach the remote environment only through the persistent-session
148
+ command your project sanctions, never a one-shot remote exec. State the
149
+ three-clause GO (execute now · drive to completion yourself · then stop and
150
+ report), require the isolation proof before anything touches a shared service,
151
+ and require the 3-section report. Never do the remote work yourself.
152
+
153
+ The full pattern is `resources/driver-subagent-pattern.md`; five worked shapes,
154
+ including the restart handoff of a live driver, are in
155
+ `resources/remote-environment-scenarios.md`.
153
156
 
154
157
  ## Restart handoff
155
158
 
@@ -170,6 +173,11 @@ Restart on a number read from the pane footer, never on a felt sense.
170
173
  disk, consolidate, for N>6 items. This skill sets the session's posture; that one
171
174
  runs a single pass.
172
175
  - `choosing-subagent-models` — the tier policy and source of the CONSTRAINTS block.
176
+ - [driver-subagent-pattern.md](resources/driver-subagent-pattern.md) — the full
177
+ remote-work pattern: the named driver, the three-clause GO, the isolation
178
+ proof, the report contract, the parent's duties, the anti-patterns.
179
+ - [remote-environment-scenarios.md](resources/remote-environment-scenarios.md) —
180
+ five worked shapes, from a long lab run to a restart handoff of a live driver.
173
181
 
174
182
  ## Failure modes
175
183
 
@@ -0,0 +1,138 @@
1
+ # The driver subagent pattern — running remote work without spending your context
2
+
3
+ Use this when the work lives somewhere your session should never go directly: a
4
+ remote host, a virtual machine, a lab box, a container, a sandboxed service. You
5
+ stay two steps removed.
6
+
7
+ ```
8
+ you (parent) holds: the question, the yes/no, a short evidence summary
9
+ └─ named driver subagent holds: commands, logs, retries, waiting, troubleshooting
10
+ └─ remote environment holds: the real system under test
11
+ ```
12
+
13
+ Your context never receives build logs, file dumps, or command transcripts. The
14
+ driver holds those and answers questions about them.
15
+
16
+ ## 1. One driver, named, continued by message
17
+
18
+ Create the driver once with an explicit `name` and an explicit `model`, then
19
+ continue it in rounds: GO, report, next GO. It keeps its remote session, its
20
+ findings, and its troubleshooting state across rounds.
21
+
22
+ Do this:
23
+
24
+ 1. `Agent(name: "vmdriver_<slug>", model: "sonnet", prompt: <round-1 GO>)`.
25
+ 2. Wait for its report.
26
+ 3. `SendMessage(to: "vmdriver_<slug>", message: <round-2 GO>)`.
27
+
28
+ Do not dispatch a fresh subagent per step, and do not dispatch one told to
29
+ "monitor until done". A subagent whose turn ends is not waiting, it is gone, and
30
+ a monitor it armed in that turn never reports. Run one driver per investigation
31
+ thread and parallelize across threads, not across the steps of one thread.
32
+
33
+ ## 2. Every GO carries the three clauses
34
+
35
+ State all three, every round:
36
+
37
+ 1. This is the GO. Execute the sequence now. It is not another round of
38
+ questions.
39
+ 2. Drive it to completion yourself. Poll, retry, troubleshoot. Do not arm a
40
+ monitor and end your turn.
41
+ 3. Then stop and report. Nothing pending.
42
+
43
+ Clause 2 is the one that decides whether the round produces work or silence.
44
+
45
+ ## 3. What a GO must contain
46
+
47
+ 1. Goal, then ordered steps, each naming the exact fields to capture before and
48
+ after.
49
+ 2. The one sanctioned way to reach the environment, named explicitly, with the
50
+ alternatives ruled out. A one-shot remote exec is usually a different shell
51
+ with a different path and a different caller identity, so it silently tests a
52
+ different thing than an interactive session does.
53
+ 3. The exact host or target, the working directories, and the revision under
54
+ test. A stale host alias that resolves elsewhere will pass every check while
55
+ measuring the wrong machine, so have the driver print the resolved hostname.
56
+ 4. An isolation gate, written as a numbered step, not as a caveat. See section 4.
57
+ 5. Fences that bind the driver and anything it dispatches: read-only version
58
+ control outside its own workspace, no installs that touch shared binaries, no
59
+ restart of a shared service, and no recursive delete flags.
60
+ 6. The report shape from section 6, including what you do not want back.
61
+
62
+ ## 4. Isolation proof before acting on a shared service
63
+
64
+ If the environment also runs a shared instance of the service, every restart,
65
+ stop, or reconfigure must target the sandboxed instance. Make the driver print
66
+ the identity of the instance it is about to touch, and confirm it is the
67
+ sandboxed one, before each such action.
68
+
69
+ Verify isolation by effect rather than by intent. The sandboxed instance and the
70
+ shared one should report different identifiers, and the test participants should
71
+ be visible to the sandboxed instance and absent from the shared one.
72
+
73
+ Sandboxing is rarely total. Check what your isolation mechanism does not cover,
74
+ and have the driver clean up by hand at teardown whatever the sandbox reaper
75
+ cannot reach.
76
+
77
+ ## 5. Long-running work in the remote environment
78
+
79
+ 1. Prefer a persistent interactive session. Working directory, environment, and
80
+ background jobs then survive between calls.
81
+ 2. Anything longer than a couple of minutes runs detached, writing its exit code
82
+ to its own file. A chained command hides the exit code. The driver polls that
83
+ file. The parent never polls.
84
+ 3. Run tests in the foreground with a bounded timeout and send output to a file.
85
+ Piping through a pager or a line filter masks the real exit status and drops
86
+ the lines that carry the stack dump.
87
+ 4. When anything looks crossed or stale, tear the session down and reconnect
88
+ rather than reasoning about which host you are on.
89
+
90
+ ## 6. The report contract, driver to parent
91
+
92
+ Three sections, in this order, and nothing else:
93
+
94
+ 1. Isolation proof. Which instance was touched, and evidence it was the
95
+ sandboxed one.
96
+ 2. Measured results, per step, with the exact fields captured before and after,
97
+ and verbatim error text wherever a command failed. "It worked" without the
98
+ fields is not a result.
99
+ 3. Explicit close. Either "stopping here as instructed, nothing pending", or the
100
+ precise point it is blocked and why.
101
+
102
+ No logs, no transcripts, no command dumps. Those stay with the driver. Ask a
103
+ follow-up round for any specific one.
104
+
105
+ ## 7. Your duties as the parent
106
+
107
+ 1. Verify, do not trust. An idle notification means a turn ended, not that the
108
+ work finished. Check the agent's state before acting on it.
109
+ 2. Spot-check one or two load-bearing claims from each report, such as a
110
+ revision, an instance identifier, or a single field value. Decide which ones
111
+ before you send the GO.
112
+ 3. If a round produces no report and no progress, stop that driver and
113
+ re-dispatch with clause 2 stated more forcefully. Do not queue another GO
114
+ behind a dead one.
115
+ 4. Never do the remote work yourself to just check quickly. That is exactly the
116
+ context this pattern protects. Ask the driver a smaller question instead.
117
+
118
+ ## 8. Anti-patterns
119
+
120
+ | Anti-pattern | What it produces | Replacement |
121
+ |---|---|---|
122
+ | Fire-and-forget subagent told to monitor | Long runs, large token spend, no output | Named driver, message rounds, clause 2 |
123
+ | One-shot remote exec instead of a session | Wrong path, wrong caller identity, wrong test | The one sanctioned access path, stated in the GO |
124
+ | A stale host alias | Results from the wrong machine, all checks green | Print the resolved hostname in the isolation proof |
125
+ | Driving behavior from a raw shell | Tests the unbound caller path, not the real one | Send the command into the participant's own pane |
126
+ | Parent reading logs to help | Spends the context the pattern exists to protect | Ask the driver for the one field you need |
127
+ | Work committed on a stale base | Invisible to everyone, diverges quietly | Cut the workspace from current trunk, check the base revision |
128
+
129
+ ## 9. Checklist before sending a GO
130
+
131
+ 1. The driver is named, and I will continue it by message rather than
132
+ re-dispatch.
133
+ 2. The GO has ordered steps with captures, the sanctioned access path, the host
134
+ and paths and revision, the isolation gate as a step, the fences, the report
135
+ shape, and the three clauses.
136
+ 3. `model` is passed explicitly.
137
+ 4. I know which one or two claims I will spot-check.
138
+ 5. Nothing in the GO can reach a shared service or install a shared binary.
@@ -0,0 +1,97 @@
1
+ # Worked scenarios — driving remote work through a driver subagent
2
+
3
+ Five shapes you will meet. Each names what the parent keeps, what the driver
4
+ owns, and the trap specific to that shape. The pattern itself is in
5
+ `driver-subagent-pattern.md`. Placeholders: `<remote-host>` is the machine,
6
+ `<your-ssh-wrapper>` is whatever persistent-session command your project
7
+ sanctions for reaching it.
8
+
9
+ ## Scenario 1 — a long lab run on a remote box
10
+
11
+ You want a multi-hour load or soak run and a verdict, not a log.
12
+
13
+ 1. Spawn `vmdriver_<slug>` with `model: "sonnet"`. The GO names the run, the
14
+ revision under test, and the exact metrics to report.
15
+ 2. The driver opens a persistent session with `<your-ssh-wrapper> <remote-host>`,
16
+ starts the run detached, and writes the exit code to its own file.
17
+ 3. The driver polls that file. You do not. You go on talking to the operator.
18
+ 4. The driver reports the three sections and stops.
19
+
20
+ Trap: a run started inside a one-shot remote exec dies with the connection, and
21
+ the failure reads as a crash in the thing under test. Start it detached from a
22
+ persistent session.
23
+
24
+ ## Scenario 2 — a reproduction that needs a sandboxed service
25
+
26
+ The bug only appears in a running system, so a source read cannot settle it.
27
+
28
+ 1. The GO orders: build the revision under test, bring up a sandboxed instance
29
+ of the service, print that instance's identity, and confirm it differs from
30
+ the shared instance before touching anything.
31
+ 2. Only then does the driver run the reproduction steps, capturing the named
32
+ fields before and after.
33
+ 3. The report leads with the isolation proof.
34
+
35
+ Trap: sandboxing usually covers the service and its data but not every process
36
+ the service spawns. Ask the driver, in the GO, to state what its sandbox does
37
+ not cover, and to hand-clean those at teardown.
38
+
39
+ ## Scenario 3 — a multi-round measurement with a parked driver
40
+
41
+ You need a baseline, a change, and a comparison, with your own judgment between
42
+ rounds.
43
+
44
+ 1. Round 1 GO: take the baseline, report the fields, stop.
45
+ 2. You read a short report, decide, and send round 2 by message. The driver
46
+ still holds its session, its build, and its earlier numbers, so round 2 costs
47
+ almost nothing to set up.
48
+ 3. Round 3 asks for the comparison in the driver's own words plus the raw fields.
49
+
50
+ Trap: re-dispatching a fresh subagent between rounds throws away the warm
51
+ environment and the earlier context, then pays to rebuild both. Continue the
52
+ named driver instead.
53
+
54
+ ## Scenario 4 — driving real agents inside the remote environment
55
+
56
+ You are testing behavior that only exists for a properly bound, running agent.
57
+
58
+ 1. The driver brings up participants in the sandboxed environment and starts
59
+ their runtimes.
60
+ 2. It exercises behavior by sending commands into each participant's own pane,
61
+ not from the raw shell. A raw shell is an unbound caller and tests a
62
+ different, usually always-failing, path.
63
+ 3. It reads results by capturing the pane and by inspecting the participant's
64
+ own state files before and after.
65
+ 4. It can ask a participant an investigative question and get reasoned findings
66
+ back. These are full agents, so delegate cognition, not just keystrokes.
67
+
68
+ Trap: a pane reported alive may still be parked at a first-run trust prompt with
69
+ no runtime behind it. Verify by pane content, never by a status field.
70
+
71
+ ## Scenario 5 — restart handoff of a live driver
72
+
73
+ Your context is near its limit and a driver is mid-investigation.
74
+
75
+ 1. Message the driver: finish the current step, write your file, report, and
76
+ stop.
77
+ 2. Record in your restart snapshot, for that driver: its name, what it owns, its
78
+ result file path, its model, the round it was on, and any state it left
79
+ behind in the remote environment such as a running job, a live session, or a
80
+ sandbox that has not been torn down.
81
+ 3. State plainly which drivers have already exited.
82
+ 4. Your successor respawns the driver with the result file as seed and resumes
83
+ at the next round.
84
+
85
+ Trap: a driver left mid-flight across a restart leaves an orphaned sandbox and a
86
+ detached job nobody owns. The snapshot line about leftover state is the part
87
+ that gets skipped and the part that costs the most.
88
+
89
+ ## Choosing between shapes
90
+
91
+ | Signal | Shape |
92
+ |---|---|
93
+ | One question, one long run | Scenario 1 |
94
+ | Behavior only reproduces in a live system | Scenario 2 |
95
+ | You must judge between rounds | Scenario 3 |
96
+ | The bound-caller path is what is under test | Scenario 4 |
97
+ | Your context is the binding constraint | Scenario 5 |
@@ -123,9 +123,30 @@ salvaged files distinct names and land the salvage somewhere durable (the main r
123
123
  **`thrum agent delete --force` is REFUSED by the CAS guard on a stale agent with an empty
124
124
  `agent_pid_start_time`** (*"expected-state premise is required … refusing rather than treating
125
125
  an empty/never-read premise as a match"*). `--force` does NOT bypass it; the guard is
126
- correct. So the reap that actually frees load is **`git worktree remove --force <path>`** —
127
- the delete best-effort-fails and leaves a **benign orphan DB row**, which you LEAVE (filtered
128
- at query per the fleet ruling; never `agent cleanup --force` to tidy them).
126
+ correct. So the reap that actually frees load is worktree removal — but salvage BEFORE you remove,
127
+ then remove PLAIN (no `--force`):
128
+
129
+ 1. Enumerate untracked+ignored `.thrum` content — BOTH flags, one flag alone misses a class
130
+ (thrum-j14q3): `git status --untracked-files=all --ignored -- .thrum`
131
+ 2. Salvage BOTH kinds to their canonical redirect paths, and `cmp`-verify each copy is
132
+ byte-identical to its source:
133
+ - `?? .thrum/agents/<agent>/sessions/*-restart.md` — the restart snapshot.
134
+ - `!! .thrum/context/<agent>.md` — gitignored, NOT daemon-held, the **only copy**;
135
+ losing it is permanent. (`_preamble.md` is regenerable — skip it.)
136
+ 3. THEN `git worktree remove <path>` — **plain, no `--force`.** ⚠️ This refusal is a REAL
137
+ backstop only for untracked/modified content (git's own rc=128 refusal) — it is NOT a
138
+ backstop for the gitignored `.thrum/context/<agent>.md` file: `git worktree remove`
139
+ silently deletes ignored-only content with exit 0, no refusal, no warning (verified by
140
+ direct reproduction). **Step 2's salvage is the ONLY protection for that file — there is
141
+ no git-level safety net behind it.** The rc=128 refusal still matters (it catches a stray
142
+ untracked restart-snapshot salvage missed, or any other unexpected untracked file), so
143
+ still read the BARE rc and still treat a refusal as a signal to investigate — just don't
144
+ mistake it for protecting the ignored-file class. (Not a pipe — a pipe returns the last
145
+ command's rc, not `git`'s.)
146
+
147
+ The delete (`thrum agent delete`) best-effort-fails and leaves a **benign orphan DB row**,
148
+ which you LEAVE (filtered at query per the fleet ruling; never `agent cleanup --force` to
149
+ tidy them).
129
150
 
130
151
  ⚠️ **After a large reap you may see a `DeadAgentSweeper` skip alert + elevated write-RPC
131
152
  latency — do NOT assume the reap caused it.** This box carries a standing single-writer
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: coordinator-dashboard-human-loop
3
+ description: "Use when the coordinator publishes or maintains a dashboard board FOR the human owner (roster, NEED queue, owner decisions, pipeline) or routes owner answers from a dashboard back to agents. Loads the human-loop mandate - a posted question is a PROMISE — it stays answerable and visible until answered, and nothing on the board disappears silently on your side."
4
+ ---
5
+
6
+ # Coordinator dashboard: the human loop
7
+
8
+ You own boards the HUMAN reads — the reference shape is the Brainstorm Steward
9
+ console: ① NEED queue (unified, oldest-first, each an action-pick) ② roster
10
+ health ③ owner decisions (pending + recently-resolved) ④ pipeline status.
11
+ Surface basics live in `using-the-dashboard` — invoke it for the CLI shapes
12
+ and the live-vs-coming honesty table. THIS skill carries the loop mandate.
13
+
14
+ ## 🔴 The mandate: posted questions don't disappear until answered
15
+
16
+ When a board carries a question for the human (an action-pick), that question
17
+ is a PROMISE with four parts:
18
+
19
+ 1. **Visible** — it stays on the board (status PENDING) until answered or
20
+ explicitly withdrawn by YOU (update its text to withdrawn, don't just
21
+ delete it).
22
+ 2. **Answerable** — while E4 is NOT landed, an action-pick cannot actually be
23
+ answered in the UI. Until then, pair every board question with its
24
+ answering channel (the NEED still routes via message/steward as today) and
25
+ say so on the board — never let the human hit a dead control.
26
+ 3. **Tracked** — check `thrum dashboard results` after every relevant nudge;
27
+ an answer arrives as a message carrying `question_id` + pick + optional
28
+ text. Unread answers + a PENDING widget = you broke the loop.
29
+ 4. **Closed** — on consuming an answer, `thrum dashboard update <widget-id>`
30
+ to flip that pick's `status` to `resolved` (and log the resolution in your
31
+ decisions list). An answered question that still shows PENDING is the
32
+ coordinator-loop failure mode. If something else wrote the board first,
33
+ your `update` can be rejected as an `expected_rev` mismatch — re-read with
34
+ `dashboard show` and resubmit against the fresh `rev` (see
35
+ `using-the-dashboard`).
36
+
37
+ ## Authoring discipline
38
+
39
+ - **One question, one action-pick.** Don't bundle three rulings into one
40
+ widget — the human answers picks discretely (that's why pick+optional-text
41
+ won over free-text).
42
+ - **Options carry the tradeoff.** Each option's label states the choice AND
43
+ its one-line consequence ("Live-only — no new migration"); the human should
44
+ not need `get` to understand a vote. Set `allow_text: true` — "B, but weigh
45
+ X" is how owners actually answer.
46
+ - **Board hygiene is part of the loop.** Stale roster stats and resolved
47
+ pipelines rot the board's credibility; refresh stat-rows when state moves
48
+ and prune sections that no longer serve. The user can delete any widget
49
+ SILENTLY (you are not nudged) — a widget that vanished was seen and
50
+ dismissed; check `show` before reposting.
51
+ - **`clear` is a reset, not an archive.** Need history? It lives in the
52
+ answer messages and your memories, not on the board.
53
+
54
+ ## Routing answers
55
+
56
+ An answer message is routed to the OWNING agent of the question (for steward
57
+ consoles: relay to the originating brainstormer, in the owner's words — the
58
+ pick plus their typed nuance). Relay the TEXT the human wrote; paraphrasing
59
+ away their nuance defeats the optional-text feature.
60
+
61
+ ## Ship gate
62
+
63
+ The USE-mandate blocks of this skill activate when E3 (web UI tab) + E2 (CLI)
64
+ land. Before that, the board renders nowhere — publishing is a daemon-side
65
+ rehearsal only, and the mandate sections are inert by the honesty table in
66
+ `using-the-dashboard`.
67
+
68
+ ## Refs to other skills
69
+
70
+ - `using-the-dashboard` — surface + CLI contract + honesty table (invoke first)
71
+ - `coordinator-running-brainstorm-cycles` — where board NEEDs come from
72
+ - `coordinator-managing-state-and-lifecycle` — state ownership the boards reflect
@@ -0,0 +1,396 @@
1
+ ---
2
+ name: coordinator-deploying-the-fleet
3
+ description: "Use when rolling a build across ALL boxes - fleet deploy, fleet roll, rolling the fleet, deploy everywhere, ship to every box, canary then the rest, promote the canary, fleet rollout, deploying after a merge batch. Orchestrates the staged multi-box order and the promotion gate between stages, and delegates each individual box to the coordinator-deploying-a-box runbook. Load this BEFORE dispatching any box."
4
+ ---
5
+
6
+ # Coordinator: Deploying a Build Across the WHOLE FLEET — staged order, canary, promotion gate
7
+
8
+ **This skill is the ORCHESTRATION layer. It does not replace
9
+ `coordinator-deploying-a-box` — it calls it, once per box.**
10
+
11
+ - **This skill answers:** which box goes first, what must be true before the next
12
+ one goes, who runs each one, and what is owed after the last one.
13
+ - **`coordinator-deploying-a-box` answers:** how to deploy one box safely.
14
+
15
+ > # 🔴🔴 LOAD `coordinator-deploying-a-box` FOR EVERY BOX — **INCLUDING THE BUILD BOX, YOUR OWN BOX. NO EXCEPTIONS.**
16
+ >
17
+ > **This is the #1 way this skill fails, and it has failed this way MORE THAN ONCE
18
+ > (Leon, 2026-09-03).** You dispatch the per-box runbook to every REMOTE coordinator —
19
+ > and then you drive your OWN box (the build box) by hand, improvising the install and restart,
20
+ > because *"I'm the coordinator, I'll just do it."* **That improvisation IS the failure.**
21
+ > The per-box runbook carries steps that are invisible until they bite:
22
+ > - 🔴 **On macOS you MUST restart via `scripts/mac-daemon-restart-via-cron.sh`, NEVER a
23
+ > bare `thrum daemon restart`** — a terminal/VSCode-parented daemon is silently DENIED
24
+ > Local Network (TCC) permission and cannot dial LAN peers. A new binary is a new cdhash,
25
+ > so the "Allow devices on the local network" prompt MUST re-fire and the operator MUST
26
+ > click Allow. **A bare restart on the build box looks successful and leaves every LAN peer
27
+ > bridge hanging "connecting" forever** (§6b-mac of the per-box runbook).
28
+ > - the backup sizing formula, the SHA-CONTAINS-the-fix ancestry check, the by-effect
29
+ > verification, the strand-gate, the phase reconcile — none of which you carry in your head.
30
+ >
31
+ > **⇒ THE MOMENT YOU REACH YOUR OWN BOX (or ANY box you drive yourself), STOP AND LOAD
32
+ > `coordinator-deploying-a-box`.** Executing-directly (not dispatching) is the ONLY thing
33
+ > that differs on your own box — you still LOAD IT AND FOLLOW IT, step for step. Reaching for
34
+ > `thrum daemon restart`, `install.sh`, or `thrum backup` on your own box WITHOUT the per-box
35
+ > runbook loaded is the red flag; if you're doing that, you have already skipped this.
36
+
37
+ **Read both. Skipping the per-box runbook because you read this one — or because the box is
38
+ your OWN and you'll "just do it" — is the failure this split is designed to prevent.**
39
+
40
+ ---
41
+
42
+ ## 0. THE ONE RULE THAT OUTRANKS EVERYTHING HERE
43
+
44
+ 🔴 **NEVER ssh-deploy INTO a box. Each box's OWN coordinator drives its OWN
45
+ deploy, LOCALLY.** (Leon, standing, binding.)
46
+
47
+ A non-interactive ssh deploy silently drops node/pnpm from PATH, `make build-ui`
48
+ exits 127, **the deploy CONTINUES and reports success**, and lands an UNBUILT
49
+ SHA. The rule exists because the failure reports green.
50
+
51
+ **So your job in a fleet roll is DISPATCH AND GATE, never execution — except on
52
+ your own box, which you EXECUTE directly (not dispatch) but STILL under
53
+ `coordinator-deploying-a-box`, loaded and followed step for step (see the 🔴🔴 block
54
+ above).** "Except on your own box" means you run the commands yourself instead of
55
+ dispatching them — it does NOT mean you improvise them from memory. If you find
56
+ yourself typing `ssh` and `make install` in the same command, stop — and if you find
57
+ yourself running `thrum daemon restart` / `install.sh` / `thrum backup` on your own box
58
+ without having loaded the per-box runbook, stop there too.
59
+
60
+ ⚠️ **CLARIFICATION — an `scp` of the signed pro-bundle zip into a box's
61
+ `~/.thrum/update/` is DISTRIBUTION OF AN ARTIFACT, not a remote build or
62
+ install, and does NOT violate this rule.** The rule forbids driving a box's
63
+ build/install/restart FROM another machine. Copying an already-built,
64
+ already-signed bundle to where that box's own coordinator will unzip and
65
+ install it FROM is the sanctioned distribution mechanism (see §4) — the box
66
+ still does its own unzip + `install.sh` + `thrum daemon restart`, locally, on
67
+ its own account. Never conflate "the bytes moved over the network" with "the
68
+ box was remote-driven."
69
+
70
+ ---
71
+
72
+ ## 1. THE ORDER — ONE fixed point, the rest composed per roll
73
+
74
+ 🔴 **ONLY ONE THING IS A STANDING RULE: the designated canary (per
75
+ `.fleet.deploy_roster`) goes FIRST, ALONE.**
76
+
77
+ **EVERYTHING ELSE IS COMPOSED FRESH EACH TIME.** Recorded rolls disagree, and
78
+ they disagree deliberately.
79
+
80
+ 🔴 **"COMPOSED FRESH" IS THE ORDER, NOT THE MEMBERSHIP.** Every box in the roster
81
+ MUST be rolled — the roster is the fleet. Compose the order per roll, never the
82
+ set. Deferring a box requires the owner's explicit sign-off for that roll and a
83
+ message to that box's coordinator that it is deferred.
84
+
85
+ ⚠️ **DO NOT WRITE "THE BUILD BOX GOES LAST" INTO YOUR PLAN AS A RULE. IT IS A
86
+ DEFAULT, AND IT HAS BEEN DELIBERATELY OVERRIDDEN** — on 2026-07-28 the build box
87
+ went SECOND, because the owner needed the changes demonstrable at a meetup.
88
+ **The owner sets the order; this skill sequences whatever order is set.**
89
+
90
+ **Sensible default when nobody says otherwise:** the canary → the remaining
91
+ boxes (parallel where they share no state) → the build box last.
92
+ **Reason to keep the build box late:** it hosts the most agents and is the
93
+ coordinator's own box, so a bad build there costs you the ability to drive the
94
+ recovery. **That is a reason, not a law.**
95
+
96
+ Each box's coordinator and deploy target are defined in `.thrum/config.json`
97
+ under `.fleet.deploy_roster` — read them there; **never hardcode box
98
+ identities in this skill.**
99
+
100
+ 🔑 **This skill runs from the fleet orchestrator's own box (the build box); the
101
+ roster lives in that box's config.** Resolve the box list and, for each box,
102
+ its `ssh_alias` / `coordinator` / `daemon_id` from that same
103
+ `.fleet.deploy_roster` entry:
104
+
105
+ ```bash
106
+ jq -r '.fleet.deploy_roster | keys[] | select(startswith("_") | not)' .thrum/config.json
107
+ # box list — underscore-prefixed
108
+ # keys (e.g. `_note`) are roster
109
+ # metadata, not boxes; filter them
110
+ jq '.fleet.deploy_roster["<box>"]' .thrum/config.json # one box's entry
111
+ # (ssh_alias, coordinator, daemon_id)
112
+ ```
113
+
114
+ **Resolve coordinators by `daemon_id` JOIN, NEVER by name** — a name is a
115
+ label somebody chose; an agent named for a box has been found resident on a
116
+ different one. **The public-line box is EXCLUDED** (public 0.10.x line — see below).
117
+
118
+ ⚠️ **At least one box's position in the roll is INCONSISTENT across rolls** —
119
+ sometimes explicitly "in order", sometimes trailing. **Ask; do not assume.** It
120
+ is always driven by its own coordinator, never dispatched around.
121
+
122
+ ### PREP-AND-HOLD — the mechanism that makes a staged roll safe
123
+ Dispatch several boxes at once to do **READ-ONLY prep only**: measure current
124
+ state, take the backup, `git pull`, capture BEFORE readings. Then:
125
+
126
+ > **"PREPARE AND HOLD. CANCEL NOTHING. DO NOT RESTART until I release you BY
127
+ > NAME."**
128
+
129
+ **Release one box at a time, by name.** This gets the slow prep done in parallel
130
+ without any box mutating itself before the canary has reported.
131
+
132
+ ⚠️ **The public-line box's coordinator runs the PUBLIC 0.10.x line and is
133
+ EXCLUDED from 0.11 rolls.** Do not dispatch it.
134
+
135
+ 🔴 **RESOLVE EVERY COORDINATOR FROM `thrum team` AT DISPATCH TIME, NOT FROM A
136
+ CACHED NAME.** A name is a label somebody chose; the authoritative method is a
137
+ `daemon_id` join — join the `daemon_id` from `.fleet.deploy_roster` against
138
+ `thrum team`'s live roster, never match on name. An agent's NAME and its
139
+ WORKTREE NAME are not its location — an agent named for a box has been found
140
+ resident on a different one, and obeying that name would have driven the
141
+ forbidden ssh path.
142
+ ⚠️ **`thrum agent list --all` returns ZERO for agents that certainly exist** —
143
+ do not use it as your roster instrument, and do not read its empty output as an
144
+ absence.
145
+
146
+ ---
147
+
148
+ ## 2. THE PROMOTION GATE — what the canary must prove before anyone else goes
149
+
150
+ **Do not release stages 2-5 on "the canary finished." Release them on these,
151
+ each measured BY EFFECT on that box:**
152
+
153
+ - Installed binary `--version` **equals the pin exactly** — not "close", not
154
+ "the branch".
155
+ - Daemon restarted with a **NEW PID**, and `daemon status` reports the pin.
156
+ ⇒ **MERGED == INSTALLED == RUNNING, all three agreeing.**
157
+ - Schema **before AND after** — proves migrating-vs-not by effect rather than
158
+ from the delta.
159
+ - UI built for real (module count, signed) — **not the exit-127 shape**.
160
+ - Built from a **CLEAN detached worktree at the pin**, `git status --porcelain`
161
+ empty ⇒ contamination closed BY CONSTRUCTION, not by inspection.
162
+ - 🔑 **`sync_checkpoints` count BEFORE and AFTER, and they match.** A reset is
163
+ the silent peer-stranding failure that has **no repair command**. **This check
164
+ is worthless without a BEFORE reading** — take your own; do not skip it
165
+ because the canary's was clean.
166
+ - `crash-fatal.log` — 0 bytes, or read it before declaring success.
167
+
168
+ ⚠️ **"CANARY GREEN" IS NOT "ALL THREE PASSES PROVEN."** The canary usually
169
+ proves the BINARY pass only. Say which passes you verified, every time.
170
+
171
+ 🔴 **THE CANARY RESULT DOES NOT TRANSFER. EVERY BOX IS MEASURED ON ITS OWN
172
+ NUMBERS.** Measured instance: the canary came back green while another box
173
+ had **207 matured tombstones over live agents**. The canary proves the BUILD is
174
+ sound; it says nothing about any other box's state. **Do not let a green canary
175
+ talk a box out of taking its own BEFORE readings.**
176
+
177
+ ⏱ **GIVE A CONVERGENCE CHECK LONG ENOUGH TO DISCRIMINATE.** A short wait cannot
178
+ tell "the fix works" from "it had already converged before you looked." One roll
179
+ made a full 3-minute wait a hard requirement after a too-short check produced an
180
+ uninterpretable pass. **Pick the interval from what the mechanism needs, not
181
+ from impatience.**
182
+
183
+ ### The go/no-go has never been a fixed number — get it from the owner
184
+ The go/no-go has never been a fixed number. **State YOUR
185
+ bar explicitly before promoting, so it can be challenged.** An unstated bar is
186
+ indistinguishable from no bar.
187
+
188
+ ---
189
+
190
+ ## 3. THE THREE PASSES — a "deploy" silently conflates three independent things
191
+
192
+ **Every box owes all three. They fail independently and a box can be current on
193
+ one and month-stale on another.**
194
+
195
+ | Pass | What it updates | Who runs it |
196
+ |---|---|---|
197
+ | 1. install the pre-built, Developer-ID-signed pro-bundle via its own `install.sh`, **then** `thrum daemon restart` | the **binary** | the box's own coordinator |
198
+ | 2. `thrum roles refresh` **then** `thrum roles deploy` | **role preambles** | the box's own coordinator |
199
+ | 3. plugin refresh / reinstall (installs the BUNDLE's plugins — already made correct/complete by the plugin-bundle spine — rather than the box's own checkout) | **skills, commands, hooks** | 🔴 **the OPERATOR (Leon)** — a coordinator CANNOT self-serve this |
200
+
201
+ 🔴 **THE BOX NEVER BUILDS OR SIGNS ITS OWN BINARY. IT INSTALLS A DELIVERED
202
+ ARTIFACT.** `make install` (build-from-local-checkout-and-sign) is retired from
203
+ the Pass-1 fleet/box binary-update path. The box's job is: obtain the pro-bundle
204
+ zip (see §4), unzip it, run its `install.sh`, restart the daemon. Nothing on the
205
+ box compiles Go or codesigns.
206
+
207
+ 🔴 **PASS 2 ORDER IS LOAD-BEARING AND NOT OBVIOUS:** `refresh` FIRST (shipped
208
+ templates → role template), THEN `deploy` (role template → per-agent preamble).
209
+ **Running `deploy` alone re-renders agents from the OLD templates and reports
210
+ success.**
211
+
212
+ 🔴 **RUN PASS 2 BETWEEN THE BUNDLE INSTALL AND THE DAEMON RESTART, ALWAYS:**
213
+ bundle `install.sh` → `thrum roles refresh` → `thrum roles deploy` → daemon
214
+ restart. Do the refresh+deploy the instant `install.sh` finishes and BEFORE
215
+ restarting the daemon, so the current preamble is already in place when any
216
+ agent next runs `thrum prime`, restarts, or checks its preamble.
217
+
218
+ 🔴 **PASS 2 IS MANDATORY WHENEVER THE BUILD CHANGES
219
+ `internal/context/roleconfig/templates/` OR the compiled-in preamble.**
220
+ Preambles are RENDERED, not read live — installing a binary changes the source
221
+ and changes **nothing any agent reads**. There is **no staleness signal**: a box
222
+ serving a month-old render looks identical to a current one.
223
+
224
+ 🔴 **PASS 3 REQUIRES A PLUGIN VERSION BUMP OR IT REACHES NOBODY.** The install
225
+ cache is keyed by version; an unchanged version means the installer skips the
226
+ re-copy and every skill edit stays invisible. Invoke the `plugin-update` skill;
227
+ do not improvise it.
228
+
229
+ ### 🔴 THE BOX'S **REPO CHECKOUT** MUST BE AT THE PIN BEFORE PASS 3 — THE BINARY BEING AT THE PIN IS NOT SUFFICIENT
230
+
231
+ The plugin source is a **LOCAL PATH**, so pass 3 reads the checkout, not the
232
+ binary — a box whose binary is at the pin can still install a plugin version
233
+ one bump behind if its working tree lags.
234
+
235
+ ⚠️ **AND THE BOX'S OWN VERIFICATION CANNOT SEE IT.** It compared installed
236
+ skills against its own source, byte-for-byte, zero mismatches — and
237
+ **both sides were the stale version.** That proves the box is internally
238
+ consistent WITH ITSELF and is structurally incapable of detecting a stale
239
+ source. Same shape as render-fidelity-vs-doctrine-currency for preambles, except
240
+ **pass 3 has no equivalent of the ancestry check**, so nothing warns you.
241
+
242
+ 🔴 **IT IS SELF-SEALING.** The cache is keyed by version, so at the stale version
243
+ the installer sees a match and **skips the re-copy permanently.** Re-running the
244
+ refresh does NOT fix it. Only bringing the checkout to the pin and re-running
245
+ does.
246
+
247
+ **⇒ Before handing a box to the operator for pass 3, verify BY EFFECT that all
248
+ FOUR manifests read the pin's version.** A partial bump is worse than none — the
249
+ runtimes then disagree about which build they are. And name the cache directory
250
+ **explicitly** when verifying; never `ls | tail -1`, because version directories
251
+ do not sort the way you expect (`…1.12` sorts BELOW `…1.3` and `…1.5`, and
252
+ several versions coexist).
253
+
254
+ **When reporting a deploy, name which passes you verified.** "Deployed" that
255
+ silently covers all three is how staleness hides.
256
+
257
+ ---
258
+
259
+ ## 4. PRE-FLIGHT — before dispatching box 1
260
+
261
+ 0. 🔴 **FIRST ACTION, NON-OPTIONAL: `git fetch origin` then fast-forward-only
262
+ pull the repo root to `origin/<branch>`.** If the pull is not
263
+ fast-forwardable, STOP and surface it — never blind-merge in a shared tree.
264
+ Do this before reading any other pre-flight state below; a stale checkout
265
+ makes every subsequent check (trunk-green, pin, schema) a check against the
266
+ wrong tree.
267
+ 1. **Is trunk green?** Trunk-green is defined as `make gate` passing. A green
268
+ lane, a green package, or a clean dual review is **not** that claim.
269
+ ⚠️ **Publishing a green without naming its invocation is the most common
270
+ error here:** 0-of-6 and 0-of-15 are different claims in identical words.
271
+ 2. **PIN AN EXACT SHA. NEVER "tip".** Tip moves under boxes mid-roll — two boxes
272
+ told to deploy "tip" ship different code and both report success.
273
+ 3. **Migrating or not?** Compare `CurrentVersion` at the pin against each box's
274
+ running schema. A migrating roll pulls in the whole heavy-migration section
275
+ of the per-box runbook; a non-migrating one does not.
276
+ 4. **Forward-only:** each box's current SHA must be an ancestor of the pin.
277
+ Pair the check with a control that MUST refuse (an invented SHA).
278
+ 5. **Backup sizing** — the per-box runbook carries the formula. A percentage
279
+ rule is not it, and a stale `current/` answers "are we protected?" with a
280
+ wrong yes.
281
+ 6. **Announce the pin and the order** to every coordinator before starting, so
282
+ nobody deploys a different SHA.
283
+ 7. 🔴 **BUILD ONCE, DISTRIBUTE THE SAME ZIP TO EVERY BOX.** The build box
284
+ produces exactly ONE `make pro-bundle` at the pinned SHA. That
285
+ single zip — never rebuilt per box — is what every box in the roll installs
286
+ from. The staged roll therefore orchestrates three phases in order:
287
+ **build once** (the build box, `make pro-bundle` at the pin) **→ distribute**
288
+ (hand the same zip to each box's coordinator, via drop-folder or direct
289
+ hand-off — see below) **→ each box installs from the delivered bundle**
290
+ (§3 Pass 1). No box re-derives the artifact; every box's binary is
291
+ byte-identical because it came from the same zip.
292
+ - **Distribution mechanism 1 — drop-folder:** the built zip is placed where
293
+ each box's own sync/fetch path picks it up into its local
294
+ `~/.thrum/update/` for that box's coordinator to find and unzip.
295
+ - **Distribution mechanism 2 — hand-off:** the coordinator running the roll
296
+ hands the zip directly to a box's coordinator (e.g. `scp` into
297
+ `~/.thrum/update/` — see §0's clarification: this is artifact
298
+ distribution, not remote build/install).
299
+ - Whichever mechanism is used, confirm the box installed from the SAME
300
+ bundle (matching version/checksum) that was built at the pin — never a
301
+ locally-reconstructed one.
302
+ - 🔴 **NEVER pass `PRO_BUNDLE_VERSION=$(git describe)` when building the pro
303
+ bundle — trust the Makefile default** (`v0.11.0-rc.1` as of this writing,
304
+ re-check the current default before citing it). `git describe` falls back
305
+ to the nearest ancestor tag when the exact commit isn't tagged, which can
306
+ silently produce a WRONG version string — this is exactly how a stale
307
+ `v0.10.6-rc.4` string previously shipped in a v0.11 build: the
308
+ `v0.11.0-rc.1` tag was stranded on an unmerged release branch, so
309
+ `git describe` fell back to an ancestor v0.10.6 tag.
310
+
311
+ ---
312
+
313
+ ## 5. DISPATCHING A BOX — what the message must carry
314
+
315
+ 🔑 **Resolve the box's `ssh_alias` / `coordinator` / `daemon_id` from
316
+ `.fleet.deploy_roster` in `.thrum/config.json` before dispatching** (see §1) —
317
+ never from a remembered name. Join the coordinator by `daemon_id` against
318
+ `thrum team`'s live roster, NEVER by name.
319
+
320
+ Give each coordinator, explicitly:
321
+
322
+ - **The exact pinned SHA** and the instruction to build from a clean detached
323
+ worktree at it.
324
+ - **An instruction to load `coordinator-deploying-a-box` and follow it** — not a
325
+ summary of it. ⚠️ **A summary you wrote reads as complete precisely because
326
+ you wrote it**, and its omissions are silent.
327
+ - **Which passes it owes** (1 and 2; pass 3 is the operator's).
328
+ - **Its own BEFORE readings** — `sync_checkpoints`, schema, PID.
329
+ - **That acceptance is BY EFFECT, never by exit code.**
330
+ - **That it must report which passes it verified**, separately.
331
+
332
+ ---
333
+
334
+ ## 6. AFTER THE LAST BOX
335
+
336
+ - 🔴 **ROSTER COMPLETENESS GATE — first.** Re-enumerate the roster from config,
337
+ not from propagated deploy-state (a box with a wedged sync never propagates its
338
+ row). Confirm every box is rolled on the pin or owner-deferred with its
339
+ coordinator notified. Name each box and its disposition in the roll-complete
340
+ report.
341
+ - **Agent phase reconciliation, per box.** A restart leaves recorded `phase`
342
+ disagreeing with reality — live agents read `stale`, dead ones can read
343
+ `active`. Phase is local state and does not cross the peer boundary, so every
344
+ box runs it against its own daemon.
345
+ - **Restart the orchestrators and long-lived agents** so they pick up the new
346
+ preambles and skills. 🔴 **Agents adopt a new preamble ON THEIR NEXT RESTART,
347
+ not immediately** — a live agent keeps behaving per the identity it was born
348
+ with. Track adoption by session birth time, never file mtime.
349
+ - **Pre-release test suite — CHECK WHETHER IT IS OWED; do not assume it is.**
350
+ It is documented as a gate for cutting an RC/release, and a remote box runs it
351
+ after fleet deploys by standing arrangement. ⚠️ **A search of past rolls did
352
+ NOT establish it as a mandatory step for a routine binary/preamble/plugin
353
+ roll.** Read the run index: if its newest entry predates this deploy, the suite
354
+ has not run against what is now deployed — then decide, with the owner,
355
+ whether this roll warrants one. **Stated as a gap rather than invented as a
356
+ rule.**
357
+ - **Update the deploy-state record** — per box: serving SHA, schema, AS OF, and
358
+ how established. **Do not normalise every row to one SHA**; an unmeasured row
359
+ is not a measured negative, and a tidied row is indistinguishable from a fresh
360
+ measurement.
361
+ - **Sweep for SHA-anchored rules that just went stale.** A superseded SHA has
362
+ two roles that fail in OPPOSITE directions: as a GUARD it refuses legitimate
363
+ work and presents as caution; as a TARGET it validates the wrong artifact and
364
+ produces **a green that means nothing**. The target direction is the dangerous
365
+ one and it is the one a guard-sweep misses. Prefer re-deriving the live build
366
+ at run time over re-anchoring, which only re-arms the trap for the next roll.
367
+
368
+ ---
369
+
370
+ ## 7. HOLDS
371
+
372
+ - **A fleet deploy is the OWNER'S CALL.** A lifted prohibition is not a
373
+ permission.
374
+ - **Any box may hold its own deploy** and must say why. A box-local blocker that
375
+ does not touch the deploy path is not a reason to hold — check whether the
376
+ runbook's acceptance checks actually need the thing that is broken.
377
+ - **If the canary fails, the roll stops.** Do not promote on a partial result.
378
+ - **Deploy often, in small batches.** A large roll bundles unrelated changes, so
379
+ when a box misbehaves afterwards, attribution across a hundred commits gets
380
+ expensive fast.
381
+
382
+ ---
383
+
384
+ ## Red flags — STOP
385
+
386
+ - `ssh` and `make install` in the same command.
387
+ - Dispatching a box by NAME without resolving its coordinator at dispatch time.
388
+ - Promoting past the canary on "it finished" rather than on the §2 checks.
389
+ - Reporting "deployed" without saying WHICH of the three passes was verified.
390
+ - Running `roles deploy` without `roles refresh` first.
391
+ - Expecting skill changes to reach agents without a plugin version bump.
392
+ - Deploying "tip" instead of a pinned SHA.
393
+ - Reading an installer's exit 0 as proof the binary landed.
394
+ - Taking an `after` reading with no `before` — especially `sync_checkpoints`.
395
+ - Summarising the per-box runbook into a dispatch instead of telling the
396
+ coordinator to load it.
@@ -120,8 +120,8 @@ rediscovering these the hard way:
120
120
  7. Build+test the MERGED-tree result as a SEPARATE condition — neither gate
121
121
  currently runs a build of the actual post-merge tree; a clean pre-merge
122
122
  build/test does not prove the merged result compiles or passes.
123
- 8. Use `rm -r`, NEVER `rm -rf`, and use `git worktree remove --force` to drop a
124
- worktree. A broad `ask` rule on `rm -rf *` outranks any narrow `/tmp` allow,
123
+ 8. Use `rm -r`, NEVER `rm -rf`; use plain `git worktree remove` to drop a worktree, never
124
+ `--force` as the default (see item 12 below). A broad `ask` rule on `rm -rf *` outranks any narrow `/tmp` allow,
125
125
  so `rm -rf` raises a human permission prompt EVERY time regardless of path —
126
126
  it stalls gate sub-agents mid-run waiting on a keystroke. `rm -r` runs free. Do NOT widen the ask rule
127
127
  to work around this; that entry is the only deletion protection on the box.
@@ -154,7 +154,11 @@ rediscovering these the hard way:
154
154
  HEAD is a gate-produced merge reachable from no ref and removal orphans
155
155
  it. Usually fine (a gate merge is reproducible by redoing it) but say so
156
156
  in your report rather than doing it silently.
157
- Then `git worktree remove --force <wt>`.
157
+ Then `git worktree remove <wt>` — **plain, no `--force`.** Its rc=128 refusal backstops
158
+ ONLY untracked/modified content (see rule 8 on `rm -r`) — it does NOT cover gitignored
159
+ content, which git removes silently with no refusal; a clean rc=0 is not proof nothing of
160
+ value was in there (see coordinator-assessing-agent-completion's reap procedure). `--force`
161
+ is a justified override only, never the default (see coordinator-merging-code §7a).
158
162
 
159
163
  ## Trigger-directory skip logic
160
164
 
@@ -110,8 +110,8 @@ dispatch time — hand this list verbatim:
110
110
  (c) only if a pipeline is unavoidable, use `${pipestatus[1]}` on zsh /
111
111
  `${PIPESTATUS[0]}` on bash — and STATE WHICH SHELL, because the same
112
112
  string means different things in each.
113
- 8. Use `rm -r`, NEVER `rm -rf`, and use `git worktree remove --force` to drop
114
- a worktree. A broad `ask` rule on `rm -rf *` outranks any narrow `/tmp`
113
+ 8. Use `rm -r`, NEVER `rm -rf`; use plain `git worktree remove` to drop a worktree, never
114
+ `--force` as the default (see item 12 below). A broad `ask` rule on `rm -rf *` outranks any narrow `/tmp`
115
115
  allow, so `rm -rf` raises a human permission prompt EVERY time regardless
116
116
  of path and stalls gate sub-agents mid-run waiting on a keystroke. `rm -r`
117
117
  runs free. Do NOT widen the ask rule to work around this; that entry is
@@ -149,7 +149,11 @@ dispatch time — hand this list verbatim:
149
149
  HEAD is a gate-produced merge reachable from no ref and removal orphans
150
150
  it. Usually fine (a gate merge is reproducible by redoing it) but say so
151
151
  in your report rather than doing it silently.
152
- Then `git worktree remove --force <wt>` (see rule 8 on `rm -r`). Rule 11's
152
+ Then `git worktree remove <wt>` — **plain, no `--force`.** Its rc=128 refusal backstops
153
+ ONLY untracked/modified content (see rule 8 on `rm -r`) — it does NOT cover gitignored
154
+ content, which git removes silently with no refusal; a clean rc=0 is not proof nothing of
155
+ value was in there (see coordinator-assessing-agent-completion's reap procedure). `--force`
156
+ is a justified override only, never the default (see coordinator-merging-code §7a). Rule 11's
153
157
  hazard notice on its own — teardown can destroy uncommitted state — reads
154
158
  as a reason NOT to tear down. A warning without a procedure does not
155
159
  produce caution; it produces paralysis plus litter.
@@ -16,8 +16,16 @@ discipline (which handles routine, cost-driven restarts).
16
16
  Read `.thrum/agents/<you>/watch_params.json` at the top of every cycle. For
17
17
  every agent in your `roster`:
18
18
 
19
- 1. **Keep it from stopping.** Capture its pane. If it's blocked on a
20
- permission modal or appears stalled, judge whether to unblock it.
19
+ 1. **Keep it from stopping.** Capture its pane with the shipped script (see
20
+ "Driving your cycle" for the `thrum tmux capture` + SSH-fallback method) —
21
+ never capture ad-hoc or key on fixed prompt phrases. A failed or empty
22
+ capture is NOT a dead or idle pane: fall back and re-read; never skip a
23
+ roster member because its capture failed. If it's blocked on a permission
24
+ modal or appears stalled, judge whether to unblock it. A member that is
25
+ *persistently* unwatchable (fails every capture path, isolated,
26
+ unreachable) is escalated to your `parent` every cycle until a coordinator
27
+ or human removes it from your roster — never dropped, never silently
28
+ given up on (see Escalation).
21
29
  2. **Watch its context% and warn.** Read its ctx% from YOUR OWN tmux
22
30
  capture of its status bar — **never from a figure it claims about
23
31
  itself** (a message, a memory-footer line, anywhere). If it's getting
@@ -37,15 +45,19 @@ every agent in your `roster`:
37
45
  ## The modal bright line (non-negotiable)
38
46
 
39
47
  - Approve ONLY clearly-safe, recognized, non-`rm`/non-`--force` modals.
40
- - Anything containing `rm`, `--force`, or anything unrecognized → refuse or
41
- escalate to your `parent`. Never approve on the merits — this is a
42
- pattern check, not a judgment call.
43
- - Never approve blind: read the actual command text first.
48
+ - `rm`, `--force`, or an unrecognized command → refuse or escalate to your
49
+ `parent`. That filter is mechanical.
50
+ - Within the safe set, judge the COMMAND TEXT, not the modal's phrasing: read
51
+ the command and approve if it is clearly-safe and recognized whatever wording
52
+ the modal uses, else cancel or escalate. Never key on fixed prompt phrases;
53
+ never approve blind.
44
54
  - Verify by re-capture after acting — never trust exit status alone.
45
55
  - To unblock a REMOTE roster member's modal, use `thrum tmux send <agent> " "`
46
56
  (proxies by agent name to the owning peer daemon and appends Enter, so a
47
- space+Enter confirms the default-focused option) — never `thrum tmux key`,
48
- which is local-socket-only and errors on a remote agent. This grants only
57
+ space+Enter confirms the default-focused option); for a specific selection
58
+ use `thrum tmux send <agent> --keys <K1,K2,...>`. `thrum tmux send` is
59
+ peer-routed, so it works identically local or remote (fleet-wide) — this is
60
+ the replacement for the retired local-only tmux-key primitive. This grants only
49
61
  the leftmost/default option; the bright line above still applies. From the
50
62
  CLI/peer-router path this send is QUEUED behind the conservative
51
63
  monitor-silence wait, not instant — don't re-send while waiting it out.
@@ -109,6 +121,30 @@ is the entire reason the watch exists — and a blocked coordinator cannot
109
121
  receive its own escalation. The escalation would arrive at the one inbox
110
122
  guaranteed not to be read.
111
123
 
124
+ ### An unwatchable roster member: escalate every cycle until it is removed
125
+
126
+ A roster member you **cannot watch at all** — capture fails by every path
127
+ (local proxy *and* SSH fallback), the agent is fully isolated, or its owning
128
+ daemon is unreachable so you cannot even message it — is NOT a member you may
129
+ drop, mark dead, or fall silent on. It stays in your roster and it stays your
130
+ duty.
131
+
132
+ - **Escalate it to your `parent` every cycle it remains unwatchable —
133
+ repeatedly, not once.** A single "fully isolated" notice that you then stop
134
+ repeating is exactly the silent-give-up this archetype exists to prevent:
135
+ the roster member goes dark with nobody positioned to notice. Re-send the
136
+ escalation on each tick it is still unwatchable. (This is a standing
137
+ per-cycle duty, unlike the one-shot declared-intent nudge.)
138
+ - **You never remove it from your own roster.** Roster membership is
139
+ coordinator-owned config in `watch_params.json`, which you only ever read.
140
+ Only a **coordinator or the human** may remove an agent from your roster —
141
+ the signal that it no longer needs watching. Until that removal lands in
142
+ `watch_params.json`, you keep escalating.
143
+ - **Do not borrow the empty-roster shutdown rule here.** "Shut yourself down"
144
+ applies only when your *whole* roster is empty/missing. One unwatchable
145
+ *member* out of several is not that case: keep watching the rest, keep
146
+ escalating the unwatchable one.
147
+
112
148
  ## Declared-intent reminders (duty #4)
113
149
 
114
150
  Each cycle, for each roster member, judge whether the last assistant turn
@@ -63,12 +63,16 @@ ssh_destination() {
63
63
 
64
64
  run_local() {
65
65
  case "$MODE" in
66
- # "${EXTRA_ARGS[@]:-}" not "${EXTRA_ARGS[@]}": a DECLARED-EMPTY array
67
- # subscripted with [@] is a fatal unbound-variable under `set -u` on
66
+ # "${EXTRA_ARGS[*]:-}" not "${EXTRA_ARGS[*]}": a DECLARED-EMPTY array
67
+ # subscripted with [*] is a fatal unbound-variable under `set -u` on
68
68
  # bash <4.4 (a real bug, not a style choice -- `key` mode with zero
69
69
  # key args crashes on this fleet's /bin/bash 3.2.57 without the ":-").
70
+ # key mode delivers named tmux keys via `thrum tmux send --keys` (the
71
+ # peer-routed replacement for the retired local-only tmux-key primitive);
72
+ # EXTRA_ARGS (one key per element) is comma-joined into a single --keys
73
+ # value inside a subshell so IFS="," never leaks into the caller.
70
74
  capture) thrum tmux capture "$AGENT" 2>&1 ;;
71
- key) thrum tmux key "$AGENT" "${EXTRA_ARGS[@]:-}" 2>&1 ;;
75
+ key) thrum tmux send "$AGENT" --keys "$(IFS=,; printf '%s' "${EXTRA_ARGS[*]:-}")" 2>&1 ;;
72
76
  esac
73
77
  }
74
78
 
@@ -77,7 +81,7 @@ run_ssh() {
77
81
  dest="$(ssh_destination)"
78
82
  case "$MODE" in
79
83
  capture) cmd="thrum tmux capture '$AGENT'" ;;
80
- key) cmd="thrum tmux key '$AGENT' ${EXTRA_ARGS[*]:-}" ;;
84
+ key) cmd="thrum tmux send '$AGENT' --keys $(IFS=,; printf '%s' "${EXTRA_ARGS[*]:-}")" ;;
81
85
  esac
82
86
  # Client-side expansion of REPO_PATH/cmd into the remote command string is
83
87
  # intentional -- that's how the resolved repo path and thrum invocation
@@ -29,7 +29,7 @@
29
29
  #
30
30
  # DELIBERATE SCOPE LIMIT: this script is MECHANICAL ONLY — capture, detect,
31
31
  # emit. It never auto-escalates and never auto-approves/denies a modal
32
- # (never calls `thrum tmux key`/`thrum send`). The persistent-watcher-
32
+ # (never sends keys or text — never calls `thrum tmux send`). The persistent-watcher-
33
33
  # archetype skill is explicit that judgment (approve/cancel/escalate/nudge)
34
34
  # happens in the watcher's OWN turn when it reads the wake message this
35
35
  # script's summary line triggers, not in a script. A bash script cannot
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: using-the-dashboard
3
+ description: "Use when you want to present an interactive board to the human user (status, options, NEEDs with A/B/C picks), or when you see a dashboard mention and wonder what is actually usable today. Carries the CLI surface, the write/read discipline, and the live-vs-coming honesty table."
4
+ ---
5
+
6
+ # Using the Agent Dashboard
7
+
8
+ The Dashboard is an agent's way to put an **interactive board** in front of the
9
+ human user: typed widgets (status rows, cards, lists), optional HTML regions,
10
+ and **action-picks the user answers by clicking an option and optionally typing
11
+ nuance**. It replaces the "agent runs a TUI" pattern — it is out-of-band from
12
+ the messaging channel, so it never blocks messages and answers never scroll
13
+ away.
14
+
15
+ This is the COMMONS skill: role-agnostic surface + discipline. Role overlays
16
+ (`coordinator-dashboard-human-loop`, `brainstormer-dashboard-needs`) add the
17
+ role-specific loop on top. When an overlay and this skill conflict, the overlay
18
+ wins for role-specific behavior.
19
+
20
+ ## 🔴 Honesty table — what is usable TODAY
21
+
22
+ Check this BEFORE you promise the human anything. Stale promises are worse
23
+ than no dashboard.
24
+
25
+ | Capability | Status |
26
+ |---|---|
27
+ | `dashboard.set/get/show/clear/delete` RPCs (daemon-side) | **LANDED** on trunk (merge `6c7539236f83`); usable only against daemons BUILT after that merge — verify your daemon's build before relying on the RPCs |
28
+ | `thrum dashboard` CLI (set/show/get/update/delete/clear/results) | **LANDED** on trunk (merge `108511a0`); usable only against a `thrum` binary BUILT after that merge — verify your CLI's build before relying on these commands |
29
+ | Web UI Dashboard tab (where the human SEES a board) | **BUILT, NOT YET DEPLOYED** (E3 landed on trunk, merge `49712f87` — visible after the next deploy) |
30
+ | Answer round-trip (user clicks → message + nudge to you) | **BUILT, NOT YET DEPLOYED** (E4 landed on trunk, merge `ea95318a` — visible after the next deploy) |
31
+
32
+ **Consequence until the next deploy carries E3 and E4:** publishing a board
33
+ is STILL a no-op for the human on any box running a pre-E3/pre-E4 binary —
34
+ it renders NOWHERE there and, even where it renders, an answer cannot
35
+ return to you, even though the UI and answer-round-trip code now both exist
36
+ on trunk. Do NOT substitute a dashboard for a nudge or a direct message
37
+ unless you know the box you're talking to has deployed a build containing
38
+ BOTH E3 and E4. If the human cannot see the board or their answer cannot
39
+ reach you, the question DOES disappear — the exact failure the dashboard
40
+ exists to kill.
41
+
42
+ **Consequence, now that E2 has landed:** the `thrum dashboard ...` command
43
+ shapes below are runnable — but, per the E2 row above, only against a CLI
44
+ binary built after `108511a0`. Against an older binary, or if you're
45
+ calling the daemon directly, the raw RPCs (`dashboard.set` etc. over the
46
+ daemon socket) remain the fallback surface.
47
+
48
+ ## The board model
49
+
50
+ A dashboard is ONE structured document per agent: an envelope
51
+ `{v, agent_id, title, updated_at, rev, widgets[]}`. Six widget types:
52
+ `section` (labeled grouping), `stat-row` (label/value pairs),
53
+ `card` (title + markdown body), `list` (ordered rows),
54
+ `action-pick` (question + options + optional text — the interactive control),
55
+ `html` (sanitized rich region, authored from a FILE).
56
+
57
+ Semantics that matter to you as an author:
58
+
59
+ - **Append-only + rev'd.** Every change appends a new snapshot; `rev` increments.
60
+ Your `update` keeps a widget's position; `delete` removes it; `clear` wipes
61
+ the board. Cleared/deleted content is **GONE** — the board is ephemeral, and
62
+ the DURABLE record of any decision is the answer MESSAGES, not the board.
63
+ - **Widget cardinality (enforced):** action-pick ≤8, html ≤4, section ≤8,
64
+ card ≤24, stat-row ≤12, list ≤4, total ≤64. Lists have NO row cap.
65
+ - **Answer path:** when the user answers an action-pick, you get a **nudge**
66
+ and the answer lands as a thrum MESSAGE (payload type `dashboard.answer`,
67
+ carrying `question_id`, the option `pick`, optional `text`). You read
68
+ answers with `thrum dashboard results`, then flip the widget's status via
69
+ `thrum dashboard update` so the board shows it resolved.
70
+ - **User deletes are SILENT.** If a widget you posted disappears, the user
71
+ dismissed it — you are NOT nudged. Notice it on your next `show`; do not
72
+ repost it reflexively (the user saw it and removed it on purpose).
73
+ - **No dashboard yet is not an error.** `dashboard.get`/`dashboard.show`
74
+ succeed with `found: false` and an empty envelope when nothing has been
75
+ posted for you yet — do not treat that as a failure to recover from.
76
+ - **Concurrent writes are guarded by `expected_rev`, not merged.** If you pass
77
+ `expected_rev` and it no longer matches the board's current `rev`, the write
78
+ is rejected with `dashboard changed (rev %d, you expected %d) — re-read with
79
+ dashboard show and resubmit`. Fix: re-fetch the current document and
80
+ resubmit with the fresh `rev`. Omitting `expected_rev` skips the check
81
+ entirely (last-writer-wins) — fine for your own solo edits, risky if
82
+ something else writes the same board.
83
+
84
+ ## CLI surface (runnable now, against a `thrum` binary built after `108511a0`)
85
+
86
+ ```bash
87
+ # Publish/replace whole widgets (upsert by widget id — targets update in place)
88
+ cat > /tmp/board.md <<'EOF'
89
+ {"title": "My board", "widgets": [ ... ]}
90
+ EOF
91
+ thrum dashboard set --doc-file /tmp/board.md
92
+ thrum dashboard set --stdin < /tmp/board.md
93
+
94
+ # HTML regions come from FILES ONLY — never inline (backticks in shell strings EXECUTE)
95
+ thrum dashboard set --html-file /tmp/panel.html --html-id my-panel
96
+
97
+ # Read
98
+ thrum dashboard show # compact outline: what's on the board, what's PENDING/ANSWERED
99
+ thrum dashboard get <widget-id> # full widget JSON
100
+ thrum dashboard results # user answers (after your nudge), newest N selected, displayed oldest-first
101
+ thrum dashboard results --unanswered # only questions with no answer yet
102
+
103
+ # Mutate
104
+ thrum dashboard update <widget-id> --doc-file /tmp/new-widget.json # in place, keeps position
105
+ thrum dashboard delete <widget-id>
106
+ thrum dashboard clear # wipe the whole board (content is GONE)
107
+ ```
108
+
109
+ **Validation loop:** `set`/`update` either print `posted OK (rev N)` or fail
110
+ with one of three error shapes: a per-widget schema error naming the exact
111
+ widget id + field; a per-type cardinality error naming the widget TYPE + count
112
+ (e.g. "too many action-pick widgets: 9 (max 8)"); or the aggregate/total-cap
113
+ error, which names a CATEGORY TOKEN + count + max rather than a widget type
114
+ (e.g. "too many widgets (total_widgets): 65 (max 64)" — "total_widgets" is the
115
+ violation category, not a widget type: the per-type limits sum to 60 < 64, so
116
+ a doc breaching 64 always breaches some per-type limit too, and there is no
117
+ single offending widget type to name). On any of these: fix the file and
118
+ resubmit — do not recompute the whole board inside one invocation.
119
+
120
+ **Authoring rules with teeth:**
121
+
122
+ - Widget `id`s are load-bearing — stable across updates; `update`/`delete`
123
+ and the answer join key off them (action-picks also carry `question_id`).
124
+ - Text fields render as MARKDOWN (no raw HTML — that is the `html` widget's
125
+ job, sanitized). Keep compact fields single-line.
126
+ - HTML files: no script/style/iframe/event-handlers — the render side strips
127
+ them, but author clean anyway.
128
+
129
+ ## When to reach for the dashboard (vs a nudge vs a message)
130
+
131
+ - **Dashboard:** state the human will consume VISUALLY and possibly answer
132
+ interactively — rosters, queues of options, pipelines, NEEDs with discrete
133
+ choices. Persistent board the human checks on their own rhythm.
134
+ - **Nudge:** something needs attention NOW and is small (one line). The
135
+ dashboard does not pulse or blink; urgent + tiny = nudge.
136
+ - **Message:** conversation, anything needing prose back-and-forth, or anything
137
+ the record must keep forever (decisions live in messages).
138
+
139
+ Anti-pattern: posting a board INSTEAD of answering a direct question. The
140
+ dashboard is a surface you maintain, not a replacement for talking.
141
+
142
+ ## Refs to other skills
143
+
144
+ - `coordinator-dashboard-human-loop` — coordinator overlay: owning a board for
145
+ the human, the human-loop mandate
146
+ - `brainstormer-dashboard-needs` — brainstormer/researcher overlay: authoring
147
+ NEEDs as answerable action-picks
148
+ - `using-the-queue`, `using-thrum-state` — sibling agent-tied local surfaces
@@ -1,16 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
3
  "_supersedes": "the earlier watcher-settings allowlist spec (single-watcher restart-flow scope)",
4
- "_comment": [
5
- "This is the canonical, code-consumed source for the Thrum ordinary-operation allowlist (allowlist).",
6
- "TWO-CAVEAT SPLIT (owner ruling) — read both before editing this file:",
7
- " 1. SUPERSEDED: the original spec's 'scoped to one watcher's restart flow, not a general starting point' framing is superseded. This IS the general, code-consumed source every runtime generator draws from.",
8
- " 2. STILL BINDING, NOT SUPERSEDED — BUT THE MODEL CHANGED (owner ruling via the fleet coordinator, allowlist consolidated round): command_patterns is no longer an enumerated per-subcommand list. The owner ruled ALL thrum commands are globally allowlisted — 'thrum' itself is the ONE entry, representing the whole command FAMILY (any subcommand). This is still bounded, never a blanket Bash(*): the ONLY thing that admits a candidate is an exact executable-token match (bare 'thrum', or the Part-C canonical-install-path forms) at an executable boundary, and the fail-closed metachar/alt-exec guard (containsForbiddenFragment + the alt-exec check in matchesCommandPattern) is now the PRIMARY scoping boundary, not a secondary one — it alone is what stops sh -c/bash -c, command substitution ($(...)/backticks), compound commands (&&, ;, |, ||), and alternate executable paths (./thrum, /tmp/thrum, arbitrary absolute paths, suffix/prefix lookalikes like thrum-evil). Every generator consuming this file must render the FAMILY grant in its native syntax (Claude/OpenCode: any-subcommand pattern, no shell composition; Codex: native profile only, §5.1, no invented Bash allowlist — command_patterns is not a Codex concept) and prove the negative controls above still hold — see each generator's test file.",
9
- "READ-PATH INVARIANT: read_paths below (including the recursive '.thrum/**'-shaped grant every generator renders from '.thrum') MUST NEVER be allowed to carry secrets. .thrum is granted broad READ access across every supported runtime by this file — if any future change adds credentials, tokens, or private keys under .thrum, this allowlist makes them world-readable to every runtime this file feeds. Keep secrets out of .thrum, not out of this file.",
10
- "GLOBAL READ PATHS (allowlist round-4, owner P0): global_read_paths grants ABSOLUTE, home-relative directories outside the project root — distinct from read_paths above, which are project-relative. Per-runtime because the underlying installed-plugin-cache layout differs by runtime (no shared portable form exists): claude = ~/.claude/plugins/cache/thrum (the thrum marketplace/plugin's OWN cache subtree, version-hash-stripped — NEVER pin a specific installed <version> segment, and never grant the broader ~/.claude/plugins/cache root, which also holds OTHER installed marketplaces' plugin payload); codex = ~/.codex/skills (Codex's flattened global skill install dir — granted in full because Codex's permission syntax has no glob/prefix-match primitive to scope it to thrum-named skills only; it also exposes non-thrum skill dirs as an accepted, documented trade-off) + ~/.codex/plugins/cache/thrum-marketplace/thrum (the versioned plugin-cache mirror, same version-hash-stripping rule as Claude's). OpenCode needs NO entry here — its own installer (opencode-plugin/src/installer.ts, opencodeGlobalReadPaths) already derives its global skill/command paths dynamically from its own resolved configDir at generation time, so a static path here would be redundant and could drift from the real value.",
11
- "OWNER-AUTHORIZED EXCEPTION — /private/tmp (owner ruling, watcher-recovery P0; scope CONFIRMED all-runtimes by the fleet coordinator the same day — owner's wording was 'global configs for all agents', the operational-artifact class is runtime-independent): owner_authorized_exceptions grants EVERY supported runtime broad READ access to /private/tmp/* DESPITE it being a world-writable directory — explicitly ruled acceptable by the owner for coordinator/watcher operational artifacts. This is a DELIBERATE exception, not a template for widening elsewhere: it grants READ/ACCESS ONLY — never execute, never shell-interpolation, never a sibling root like /private or /private/tmpfoo, never write/delete. Codex renders this via its NATIVE filesystem-read profile mechanism, never as an invented Bash-pattern entry (still-binding caveat above). Every generator consuming this list must prove those boundaries by negative-control test AND by-effect (a real read actually succeeds; a real write/exec attempt actually fails), not merely by the shape of the grant string.",
12
- "~/.local/bin OWNER CORRECTION (owner ruling, consolidated round): global_read_paths.<runtime> gains '~/.local/bin' for ALL THREE runtimes — the install location of the thrum binary itself, so PATH-resolved 'thrum' can actually execute under each runtime's sandbox. Granted in full (not scoped to just the thrum binary file) for the same reason as Codex's flattened ~/.codex/skills grant above: none of the three permission syntaxes has a single-file-only primitive that stays portable, so the directory is the smallest expressible unit. This is a FILESYSTEM READ/ACCESS/EXEC grant on the install directory — it is a SEPARATE axis from command_patterns matching, and does NOT loosen the alt-exec guard in approved.go beyond the tight, explicitly-scoped canonical-install-path carve-out documented in that file (canonicalInstallPathPrefixes) — a lookalike executable in the same directory (~/.local/bin/thrum-evil) is still rejected by that carve-out's own boundary check even though the DIRECTORY grant here would let a runtime read/access it."
13
- ],
14
4
  "command_patterns": [
15
5
  "thrum"
16
6
  ],
@@ -22,13 +12,28 @@
22
12
  ".thrum"
23
13
  ],
24
14
  "global_read_paths": {
25
- "claude": ["~/.claude/plugins/cache/thrum", "~/.local/bin"],
26
- "codex": ["~/.codex/skills", "~/.codex/plugins/cache/thrum-marketplace/thrum", "~/.local/bin"],
27
- "opencode": ["~/.local/bin"]
15
+ "claude": [
16
+ "~/.claude/plugins/cache/thrum",
17
+ "~/.local/bin"
18
+ ],
19
+ "codex": [
20
+ "~/.codex/skills",
21
+ "~/.codex/plugins/cache/thrum-marketplace/thrum",
22
+ "~/.local/bin"
23
+ ],
24
+ "opencode": [
25
+ "~/.local/bin"
26
+ ]
28
27
  },
29
28
  "owner_authorized_exceptions": {
30
- "claude": ["/private/tmp"],
31
- "codex": ["/private/tmp"],
32
- "opencode": ["/private/tmp"]
29
+ "claude": [
30
+ "/private/tmp"
31
+ ],
32
+ "codex": [
33
+ "/private/tmp"
34
+ ],
35
+ "opencode": [
36
+ "/private/tmp"
37
+ ]
33
38
  }
34
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-thrum-pro",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Thrum multi-agent coordination plugin for Open Code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",