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.
- package/assets/skills/brainstorm-steward/SKILL.md +3 -2
- package/assets/skills/brainstormer-dashboard-needs/SKILL.md +62 -0
- package/assets/skills/conversation-only-context/SKILL.md +16 -8
- package/assets/skills/conversation-only-context/resources/driver-subagent-pattern.md +138 -0
- package/assets/skills/conversation-only-context/resources/remote-environment-scenarios.md +97 -0
- package/assets/skills/coordinator-assessing-agent-completion/SKILL.md +24 -3
- package/assets/skills/coordinator-dashboard-human-loop/SKILL.md +72 -0
- package/assets/skills/coordinator-deploying-the-fleet/SKILL.md +396 -0
- package/assets/skills/coordinator-hotpath-merge-gate/SKILL.md +7 -3
- package/assets/skills/coordinator-philosophy-merge-gate/SKILL.md +7 -3
- package/assets/skills/persistent-watcher-archetype/SKILL.md +44 -8
- package/assets/skills/persistent-watcher-archetype/resources/thrum-capture-fallback.sh +8 -4
- package/assets/skills/persistent-watcher-archetype/resources/thrum-watch-pane-capture.sh +1 -1
- package/assets/skills/using-the-dashboard/SKILL.md +148 -0
- package/assets/thrum_allowlist.json +21 -16
- package/package.json +1 -1
|
@@ -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 —
|
|
31
|
-
|
|
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>`)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
(execute now · drive to completion yourself · then stop and
|
|
150
|
-
isolation proof
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
124
|
-
|
|
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
|
|
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
|
|
114
|
-
|
|
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
|
|
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
|
|
20
|
-
|
|
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
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
48
|
-
|
|
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[
|
|
67
|
-
# subscripted with [
|
|
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
|
|
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
|
|
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
|
|
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": [
|
|
26
|
-
|
|
27
|
-
|
|
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": [
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
"claude": [
|
|
30
|
+
"/private/tmp"
|
|
31
|
+
],
|
|
32
|
+
"codex": [
|
|
33
|
+
"/private/tmp"
|
|
34
|
+
],
|
|
35
|
+
"opencode": [
|
|
36
|
+
"/private/tmp"
|
|
37
|
+
]
|
|
33
38
|
}
|
|
34
39
|
}
|