opencode-thrum-pro 0.1.15 → 0.1.17
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/commands/thrum-compact-extended.md +14 -12
- package/assets/commands/thrum-compact.md +17 -15
- package/assets/commands/thrum-prime.md +1 -0
- package/assets/skills/configure-roles/SKILL.md +0 -1
- package/assets/skills/coordinator-hotpath-merge-gate/SKILL.md +15 -6
- package/assets/skills/coordinator-maintaining-memory/SKILL.md +2 -0
- package/assets/skills/coordinator-merging-code/SKILL.md +25 -10
- package/assets/skills/coordinator-merging-code/resources/overbuild-check-prompt.md +60 -0
- package/assets/skills/coordinator-philosophy-merge-gate/SKILL.md +5 -4
- package/assets/skills/implementer-maintaining-memory/SKILL.md +2 -0
- package/assets/skills/memory-curate/SKILL.md +2 -0
- package/assets/skills/memory-current-rule/SKILL.md +121 -0
- package/assets/skills/memory-maintenance/SKILL.md +2 -0
- package/assets/skills/memory-write-discipline/SKILL.md +2 -0
- package/assets/skills/persistent-watcher-archetype/SKILL.md +142 -60
- package/assets/skills/project-hotpath-gate/SKILL.md +8 -0
- package/assets/skills/researcher-maintaining-memory/SKILL.md +2 -0
- package/assets/skills/thrum/resources/CLI_REFERENCE.md +14 -6
- package/assets/skills/thrum/resources/TMUX_SESSIONS.md +16 -8
- package/assets/skills/thrum-memory/SKILL.md +2 -0
- package/assets/skills/using-the-queue/SKILL.md +6 -7
- package/assets/skills/using-thrum-state/SKILL.md +2 -0
- package/assets/skills/using-tmux-ssh/resources/tmux-ssh +23 -2
- package/assets/skills/watcher-archetype/SKILL.md +13 -5
- package/dist/installer-assets.test.js +5 -2
- package/dist/installer.d.ts +5 -1
- package/dist/installer.js +38 -7
- package/dist/installer.test.js +53 -0
- package/package.json +4 -1
- package/dist/permission-fixture.integration.test.js +0 -506
- /package/dist/{permission-fixture.integration.test.d.ts → installer.test.d.ts} +0 -0
|
@@ -23,9 +23,9 @@ every agent in your `roster`:
|
|
|
23
23
|
roster member because its capture failed. If it's blocked on a permission
|
|
24
24
|
modal or appears stalled, judge whether to unblock it. A member that is
|
|
25
25
|
*persistently* unwatchable (fails every capture path, isolated,
|
|
26
|
-
unreachable) is
|
|
27
|
-
or human removes it from your roster —
|
|
28
|
-
|
|
26
|
+
unreachable) is told to your `parent` up to three times, then closed out
|
|
27
|
+
with one line; a coordinator or human removes it from your roster — you
|
|
28
|
+
never drop it (see Escalation).
|
|
29
29
|
2. **Watch context, warn, and compact only when exhausted.** Read ctx% from
|
|
30
30
|
YOUR OWN tmux capture of its status bar — **never from a figure it claims
|
|
31
31
|
about itself** (a message, a memory-footer line, anywhere). Reuse your
|
|
@@ -62,13 +62,74 @@ every agent in your `roster`:
|
|
|
62
62
|
|
|
63
63
|
- Approve a modal when you can explain what the command does, it fits what
|
|
64
64
|
the requesting agent is visibly doing in its pane, and a mistake would be
|
|
65
|
-
recoverable.
|
|
66
|
-
-
|
|
67
|
-
|
|
65
|
+
recoverable.
|
|
66
|
+
- Otherwise CANCEL it: answer with the modal's explicit No digit as read
|
|
67
|
+
from the pane (never Yes, never "don't ask again", never a bare Enter; if
|
|
68
|
+
the prompt has no No option at all, send Escape ONLY as the cancel, the one
|
|
69
|
+
case where Escape is used).
|
|
70
|
+
Re-capture to confirm it closed. Then send the blocked agent ONE message:
|
|
71
|
+
the exact command, why you declined it, and that it can retry in a safer
|
|
72
|
+
form. Cancelling is always safe: nothing runs from this modal.
|
|
73
|
+
- Escalate to your `parent` only when the agent re-raises the same operation
|
|
74
|
+
after a cancel and its reason still does not convince you, or when the
|
|
75
|
+
block is not something you can cancel (not a permission modal, or the
|
|
76
|
+
cancel did not close it).
|
|
77
|
+
- NEVER escalate the same condition more than three times. If three
|
|
78
|
+
escalations go unanswered, stop: cancel the modal if still open, send the
|
|
79
|
+
blocked agent one message (what the modal was, that you escalated three
|
|
80
|
+
times with no answer, and that you cancelled it), send your `parent` one
|
|
81
|
+
closing line, and do not raise that condition again unless it changes.
|
|
82
|
+
- Count each condition in one small file under your own agent dir. Resolve the dir through `.thrum/redirect` (it holds
|
|
83
|
+
the absolute path of the repo's `.thrum`; with no redirect, the repo's `.thrum` under
|
|
84
|
+
`git rev-parse --show-toplevel`; never a bare relative path, which would start a stray tree and reset the cap). The
|
|
85
|
+
key is the first 16 hex of sha256 of
|
|
86
|
+
`<agent>|<first line of the modal command, whitespace collapsed to single spaces>`, nothing else, so it is identical
|
|
87
|
+
on every tick. The count goes up by one for each escalation send to your parent or the human, and for nothing else:
|
|
88
|
+
cancels, approvals, messages to the blocked agent and the closing line are not counted. Shell state does not persist
|
|
89
|
+
between Bash calls, so derive `D` and `K` inside EACH call that uses them. No lock is needed: one watcher ticks
|
|
90
|
+
serially. Before every escalation send, run this (put the key text in single quotes; write a single quote in it as
|
|
91
|
+
`'\''`; replace `<watcher>` in the path with your own agent name before running it):
|
|
92
|
+
|
|
93
|
+
T=$(cat .thrum/redirect 2>/dev/null); [ -n "$T" ] || { R=$(git rev-parse --show-toplevel 2>/dev/null) && T="$R/.thrum"; }
|
|
94
|
+
case $T in /?*) D="$T/agents/<watcher>/esc";; *) D=;; esac
|
|
95
|
+
S='<agent>|<first command line>'
|
|
96
|
+
K=$( { printf '%s' "$S" | sha256sum || printf '%s' "$S" | shasum -a 256 || printf '%s' "$S" | openssl dgst -sha256; } 2>/dev/null | grep -o '[0-9a-f]\{64\}' | head -1 | cut -c1-16)
|
|
97
|
+
if [ -n "$D" ] && [ -n "$K" ] && mkdir -p "$D" && ! [ -d "$D/$K" ]; then
|
|
98
|
+
N=$(cat "$D/$K" 2>/dev/null); case $N in ''|*[!0-9]*) N=0;; ????*) N=9;; esac
|
|
99
|
+
if [ "$N" -ge 3 ]; then
|
|
100
|
+
if [ "$N" -eq 3 ]; then
|
|
101
|
+
printf '4\n' > "$D/$K.tmp" && mv "$D/$K.tmp" "$D/$K" && echo "capped: closing sequence now" || echo "cap store unavailable: do NOT escalate"
|
|
102
|
+
else touch -- "$D/$K"; echo "capped: silent"; fi
|
|
103
|
+
else N=$((N+1))
|
|
104
|
+
printf '%s\n' "$N" > "$D/$K.tmp" && mv "$D/$K.tmp" "$D/$K" && echo "escalation $N of 3: send it" || echo "cap store unavailable: do NOT escalate"
|
|
105
|
+
fi
|
|
106
|
+
else echo "cap store unavailable: do NOT escalate"; fi
|
|
107
|
+
|
|
108
|
+
If it prints "escalation N of 3", send the escalation: the count was bumped BEFORE the send. If it prints "capped:
|
|
109
|
+
closing sequence now", do NOT escalate: cancel the modal if still open, send the blocked agent one message, and send
|
|
110
|
+
your `parent` one closing line (the same closing line as above, to the parent), once; later ticks print "capped:
|
|
111
|
+
silent" and you do nothing. If it prints "cap store unavailable", the store failed closed (no writable absolute
|
|
112
|
+
path, no hash tool, or a failed write, including the write that closes the cap): do NOT escalate. The closing
|
|
113
|
+
actions that need no store still happen, once: cancel the modal if still open, send the blocked agent one message,
|
|
114
|
+
and send your `parent` one line that the cap store failed. Send that parent line ONCE per watcher session, not once
|
|
115
|
+
per tick and not once per condition: the first time, note "cap-store-failed line sent" in your own context; when
|
|
116
|
+
that note is already there, send nothing to the parent. A restart of the watcher loses that note, so after a restart
|
|
117
|
+
you may send the cap-store-failed line once more (the per-condition blocked-agent message is likewise sent once per
|
|
118
|
+
condition per session). Do the same for the blocked-agent message (note which condition you already told
|
|
119
|
+
it about). Do not try a marker file: the store is what failed. Then stay silent. An empty, garbage or missing count
|
|
120
|
+
file restarts that condition from 0 (by design), and changed command text is a new condition with its own count (by
|
|
121
|
+
design). There is no time-based reset: an unanswered condition stays capped. Whenever you observe the condition
|
|
122
|
+
again, refresh its file (derive `D` and `K` first, then `[ -f "$D/$K" ] && touch -- "$D/$K"`), so the 7-day prune
|
|
123
|
+
below removes only conditions not seen for 7 days. A cancel does NOT clear the count: you re-capture after every
|
|
124
|
+
cancel to confirm it closed, and that always shows the modal gone. Clear a condition only when the agent has stopped
|
|
125
|
+
raising that operation (it did not re-raise it for a full tick after your cancel, or the command text changed) or
|
|
126
|
+
the roster member is back: derive `D` and `K`, then `[ -f "$D/$K" ] && rm -- "${D:?}/${K:?}"`. A modal you cancelled
|
|
127
|
+
yourself never clears it. Prune with `find "${D:?}" -type f -mtime +7 -delete` (derive `D` first).
|
|
128
|
+
- Judge the TARGET, not the keyword:
|
|
129
|
+
`rm -rf /tmp/<dir the agent just created>` is routine cleanup; `rm -rf /tmp/*` is not. One `rm` of a stale
|
|
68
130
|
lock file by a coordinator mid-deploy is fine. A force-push, or a delete
|
|
69
|
-
whose target or purpose you can't tell, is
|
|
70
|
-
-
|
|
71
|
-
not.
|
|
131
|
+
whose target or purpose you can't tell, is a cancel.
|
|
132
|
+
- Refusing only because a keyword appeared is not allowed.
|
|
72
133
|
- Judge the COMMAND TEXT, not the modal's phrasing: read the command and
|
|
73
134
|
approve or refuse on what it actually does, whatever wording the modal
|
|
74
135
|
uses. Never key on fixed prompt phrases; never approve blind.
|
|
@@ -91,13 +152,15 @@ The procedure, every time, no shortcuts:
|
|
|
91
152
|
2. **Recognize the modal and which option is currently focused** (`❯` or
|
|
92
153
|
equivalent marks the default). Judge it against the rubric above.
|
|
93
154
|
3. **Choose keys, never text:**
|
|
94
|
-
- Default/focused option → `thrum tmux send <agent> --keys Enter`.
|
|
95
|
-
- A specific numbered option → the digit, e.g.
|
|
96
|
-
--keys 1`.
|
|
97
|
-
`
|
|
98
|
-
(`--keys Down,Enter` or `--keys Down --keys Enter`) — confirm
|
|
99
|
-
syntax with `thrum tmux send --help` if in doubt, never guess it.
|
|
100
|
-
-
|
|
155
|
+
- Default/focused option, to APPROVE only (never the cancel) → `thrum tmux send <agent> --keys Enter`.
|
|
156
|
+
- A specific numbered option → the digit, e.g.
|
|
157
|
+
`thrum tmux send <agent> --keys 1`. The keys that approve or navigate are `Enter`, `Up`, `Down`,
|
|
158
|
+
`Left`, `Right`, `Tab` and digits; they deliver in order, comma-separated
|
|
159
|
+
or repeated (`--keys Down,Enter` or `--keys Down --keys Enter`) — confirm
|
|
160
|
+
the exact syntax with `thrum tmux send --help` if in doubt, never guess it.
|
|
161
|
+
- The cancel is the explicit No digit read from the pane (see the rubric),
|
|
162
|
+
never a bare Enter and never Yes. `Escape` is not a general cancel key: it
|
|
163
|
+
is used only when the prompt has no No option at all, and only then.
|
|
101
164
|
- This send is QUEUED behind the conservative monitor-silence wait, not
|
|
102
165
|
instant — don't re-send while waiting it out. A send that exits 3 (Enter
|
|
103
166
|
WITHHELD) or 4 (verdict unknown) must NOT be retried blind — capture the
|
|
@@ -115,8 +178,8 @@ The procedure, every time, no shortcuts:
|
|
|
115
178
|
`tmux capture-pane -t <session> -p` to verify directly, bypassing the
|
|
116
179
|
daemon layer.
|
|
117
180
|
c. **SSH to the member's own box**, for a remote member or whenever (a)
|
|
118
|
-
fails with a proxy-failure signature
|
|
119
|
-
required capability`, `peer unreachable`, `circuit open, dial skipped`)
|
|
181
|
+
fails with a proxy-failure signature
|
|
182
|
+
(`rpcrouter: caller-peer lacks required capability`, `peer unreachable`, `circuit open, dial skipped`)
|
|
120
183
|
— use the shipped pair of scripts, don't hand-roll the SSH call:
|
|
121
184
|
`resources/thrum-watch-pane-capture.sh`'s `build_topology_table` /
|
|
122
185
|
`resolve_roster_targets` resolves the target's SSH destination from
|
|
@@ -124,23 +187,33 @@ The procedure, every time, no shortcuts:
|
|
|
124
187
|
against `thrum state show topology:<scope>` (`.entry.value.hostname`
|
|
125
188
|
/ `.entry.value.ssh.target` / `.entry.value.ssh.user` /
|
|
126
189
|
`.entry.value.repo_path`), then hands those already-resolved values to
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
190
|
+
|
|
191
|
+
```text
|
|
192
|
+
resources/thrum-capture-fallback.sh <capture|key> <agent> <ssh_target> <ssh_user> <repo_path> [keys...]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
which is the piece that actually runs
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
ssh "<ssh_user>@<ssh_target>" "cd '<repo_path>' && thrum tmux send '<agent>' --keys <keys>"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
— `thrum-capture-fallback.sh` does no
|
|
131
202
|
resolution of its own, so calling it directly requires you (or the
|
|
132
203
|
caller) to already have those three values. A topology gap fails
|
|
133
204
|
closed (no SSH fallback attempted) rather than guessing a wrong
|
|
134
205
|
destination. There is no per-agent SSH field in the roster/agent Go
|
|
135
206
|
schema — the destination comes from this `thrum state` topology data.
|
|
136
207
|
(There IS a real `.thrum/ssh/config` on fleet boxes — the underlying
|
|
137
|
-
host-alias file, included via `~/.ssh/config`, that makes a bare
|
|
138
|
-
<box>` resolve at all — but it is not what these scripts read; they
|
|
208
|
+
host-alias file, included via `~/.ssh/config`, that makes a bare
|
|
209
|
+
`ssh <box>` resolve at all — but it is not what these scripts read; they
|
|
139
210
|
read the resolved `ssh.target`/`ssh.user` values out of `thrum state`
|
|
140
211
|
instead.)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
212
|
+
If the modal still will not close after this ladder, or it is an
|
|
213
|
+
UNRECOGNIZED block you cannot cancel, escalate to your `parent` per the
|
|
214
|
+
rubric criteria (capped at three per condition) and include the fresh
|
|
215
|
+
capture. An unrecognized modal you CAN cancel is cancelled (the explicit
|
|
216
|
+
No digit), not escalated.
|
|
144
217
|
- Cross-peer send-keys being capability-gated is a deliberate security
|
|
145
218
|
boundary (§ladder step a above), not a bug to route around by other means
|
|
146
219
|
without checking the failure signature first. A peer can always CAPTURE
|
|
@@ -159,8 +232,9 @@ dialect `claude-tui`), captured verbatim:
|
|
|
159
232
|
```
|
|
160
233
|
|
|
161
234
|
- **Approve once** → `--keys 1`.
|
|
162
|
-
- **
|
|
163
|
-
|
|
235
|
+
- **Cancel** → the explicit No digit read from THIS pane (here that is `3`, but
|
|
236
|
+
always read it from the capture), never Escape. Only a prompt with no No
|
|
237
|
+
option at all is cancelled with `--keys Escape`.
|
|
164
238
|
- **NEVER `--keys 2`** — "Yes, and don't ask again" grants a standing
|
|
165
239
|
allow-rule beyond this one command; the modal judgment rubric above is a
|
|
166
240
|
per-command judgment, not a license to mint blanket permissions. This is
|
|
@@ -202,15 +276,20 @@ from live fleet operation, not a hypothetical.
|
|
|
202
276
|
> session requires a capability grant, or is exempt as self-targeting, is
|
|
203
277
|
> unconfirmed. If your own restart command is ever refused for a capability
|
|
204
278
|
> reason you don't expect, escalate to your parent rather than working
|
|
205
|
-
> around it
|
|
279
|
+
> around it (this counts toward the same cap per condition: at most three
|
|
280
|
+
> times in total).
|
|
206
281
|
|
|
207
282
|
## Escalation
|
|
208
283
|
|
|
209
|
-
Your `parent` (from `watch_params.json`) is who you notify
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
284
|
+
Your `parent` (from `watch_params.json`) is who you notify only when an
|
|
285
|
+
agent re-raised the same operation after your cancel and its reason still
|
|
286
|
+
does not convince you, or when the block is not something you can cancel
|
|
287
|
+
(not a permission modal, or the cancel did not close it). A modal for an
|
|
288
|
+
irreversible/high-blast-radius action (agent delete/retire/teardown,
|
|
289
|
+
messages.db, the shared binary, daemon restart, anything outside `/tmp`)
|
|
290
|
+
that you cannot judge is cancelled like any other. Anything else you don't
|
|
291
|
+
understand is cancelled, not escalated. Every escalation is capped at three
|
|
292
|
+
per condition (see the count store in the rubric). The tree is REFERENCE DATA — who to notify — not an active
|
|
214
293
|
enforcement structure. If you're the root watcher, your parent is the
|
|
215
294
|
human operator directly.
|
|
216
295
|
|
|
@@ -237,35 +316,37 @@ a name — and deliver it as a message, not pane text:
|
|
|
237
316
|
[ESCALATION] <what needs a decision>: <why>. Action needed: <what>.
|
|
238
317
|
EOF
|
|
239
318
|
|
|
240
|
-
Writing the escalation only into your own pane is not delivery — nobody reads
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
319
|
+
Writing the escalation only into your own pane is not delivery — nobody reads watcher panes. Verify delivery
|
|
320
|
+
(`thrum sent --unread`) before assuming it was seen. Send it at most three times in total: the first send when you
|
|
321
|
+
find the condition, then a later send only on a later tick while the same condition is still open and unanswered, at
|
|
322
|
+
most once per tick and never three in one tick. Then stop: never escalate the same condition more than three times. If
|
|
323
|
+
three sends go unanswered, cancel the modal if one is still open, send your parent one closing line, and do not raise
|
|
324
|
+
the condition again unless it changes. SAFE recognized modals stay approver-of-record and are approved directly,
|
|
325
|
+
unchanged — this rule applies only to escalations that need a human decision.
|
|
245
326
|
|
|
246
|
-
### An unwatchable roster member:
|
|
327
|
+
### An unwatchable roster member: tell the parent three times, then stay silent until it changes
|
|
247
328
|
|
|
248
329
|
A roster member you **cannot watch at all** — capture fails by every path
|
|
249
330
|
(local proxy *and* SSH fallback), the agent is fully isolated, or its owning
|
|
250
331
|
daemon is unreachable so you cannot even message it — is NOT a member you may
|
|
251
|
-
drop
|
|
332
|
+
drop or mark dead. It stays in your roster and it stays your
|
|
252
333
|
duty.
|
|
253
334
|
|
|
254
|
-
- **
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
335
|
+
- **Tell your `parent` when you first find it unwatchable, and again on
|
|
336
|
+
later cycles while it stays so, up to three times in total** (same count
|
|
337
|
+
store as the rubric; key: the first 16 hex of sha256 of
|
|
338
|
+
`<agent>|unwatchable|<one-line reason>`, with a short fixed reason such as
|
|
339
|
+
`capture failed` so the key is stable). Then send your `parent` one closing line and stay
|
|
340
|
+
silent about it until it changes. An unwatchable coordinator is escalated
|
|
341
|
+
to the human, per Escalation.
|
|
260
342
|
- **You never remove it from your own roster.** Roster membership is
|
|
261
343
|
coordinator-owned config in `watch_params.json`, which you only ever read.
|
|
262
344
|
Only a **coordinator or the human** may remove an agent from your roster —
|
|
263
345
|
the signal that it no longer needs watching. Until that removal lands in
|
|
264
|
-
`watch_params.json`,
|
|
346
|
+
`watch_params.json`, it stays in your roster.
|
|
265
347
|
- **Do not borrow the empty-roster shutdown rule here.** "Shut yourself down"
|
|
266
348
|
applies only when your *whole* roster is empty/missing. One unwatchable
|
|
267
|
-
*member* out of several is not that case: keep watching the rest
|
|
268
|
-
escalating the unwatchable one.
|
|
349
|
+
*member* out of several is not that case: keep watching the rest.
|
|
269
350
|
|
|
270
351
|
## Declared-intent reminders (duty #4)
|
|
271
352
|
|
|
@@ -299,11 +380,12 @@ anti-pattern already rejected for the wake-loop itself.
|
|
|
299
380
|
anything) → the reminder SUCCEEDED. Done — do not separately verify
|
|
300
381
|
the nudge marker arrived.
|
|
301
382
|
2. ONLY if the pane is IDENTICAL to your reminder-send capture **and**
|
|
302
|
-
the nudge marker is also absent → escalate to your `parent
|
|
383
|
+
the nudge marker is also absent → escalate to your `parent` (this
|
|
384
|
+
counts toward the same cap per condition: at most three times in total).
|
|
303
385
|
Check for the GENERIC arrival template when checking for the marker
|
|
304
386
|
(step 2 only) — `thrum send` injects `tmux.FormatNudge`'s
|
|
305
|
-
|
|
306
|
-
never the message body. Look for your own agent name inside that generic
|
|
387
|
+
line that starts with "New message from @<you> -- run" and then names the
|
|
388
|
+
command `thrum inbox --unread`, never the message body. Look for your own agent name inside that generic
|
|
307
389
|
line. `FormatNudge` has two branches — with a trailing `(Sent:...)`
|
|
308
390
|
suffix and without (when no send timestamp was available) — and the
|
|
309
391
|
sender name is rendered through `paneref.Agent()`, which only strips
|
|
@@ -338,11 +420,11 @@ choice of plumbing.
|
|
|
338
420
|
Register the loop as a `thrum monitor` instead — a daemon-scheduled job,
|
|
339
421
|
independent of your session, that keeps running across your restarts.
|
|
340
422
|
|
|
341
|
-
**A ready-to-use script ships with this skill** —
|
|
342
|
-
pane-capture.sh` (paired with `resources/thrum-capture-fallback.sh`).
|
|
423
|
+
**A ready-to-use script ships with this skill** —
|
|
424
|
+
`resources/thrum-watch-pane-capture.sh` (paired with `resources/thrum-capture-fallback.sh`).
|
|
343
425
|
It is generic and roster-driven: it reads your own
|
|
344
|
-
`watch_params.json`, captures each roster member's pane (local
|
|
345
|
-
capture` first, always — that already reaches remote agents by name via the
|
|
426
|
+
`watch_params.json`, captures each roster member's pane (local
|
|
427
|
+
`thrum tmux capture` first, always — that already reaches remote agents by name via the
|
|
346
428
|
normal rpcrouter proxy; see "SSH fallback" below for what the second script
|
|
347
429
|
is for), runs `thrum detect --category permission`, applies the two-capture
|
|
348
430
|
stability check, writes one report file, and emits one matchable summary
|
|
@@ -400,8 +482,8 @@ run time. Captures and their archive land under `.thrum/agents/<you>/watch-captu
|
|
|
400
482
|
(redirect-resolved), not beside the deployed script itself.
|
|
401
483
|
|
|
402
484
|
Register roster-watch.sh's own monitor the same way, matching its actual
|
|
403
|
-
output lines (`"roster capture ready:"` for a normal tick,
|
|
404
|
-
INCIDENT:"` for a capture-failure incident):
|
|
485
|
+
output lines (`"roster capture ready:"` for a normal tick,
|
|
486
|
+
`"ROSTER INCIDENT:"` for a capture-failure incident):
|
|
405
487
|
|
|
406
488
|
```bash
|
|
407
489
|
thrum monitor start --name roster-watch-<you> \
|
|
@@ -242,6 +242,14 @@ content. Append a provenance comment:
|
|
|
242
242
|
|
|
243
243
|
On decline — do NOT write. Record the skipped proposal for future runs.
|
|
244
244
|
|
|
245
|
+
## Test impact selection plan (per-merge gate)
|
|
246
|
+
|
|
247
|
+
Owner ruling, verbatim: "default ON as soon as it lands, no side-by-side; safety net = daily full gate + RDP load/regression test before deploy".
|
|
248
|
+
|
|
249
|
+
Run the per-merge test lanes through the repository's test-impact-selection plan wrapper (the gate role's documented
|
|
250
|
+
command, taking `--base`, `--head` and `--lane`), not the bare full-lane runner. It runs the plan, prints the `TIS_PLAN` line, and passes `--producer-rc` to the runner. Quote the `TIS_PLAN` line in the
|
|
251
|
+
verdict. FULL (`decision=FULL RUN`) is the normal fallback: a plan error or any fail-closed reason runs the whole lane. For a per-landing gate, a FULL RUN fallback means the full NON-race lane, never race-all.
|
|
252
|
+
|
|
245
253
|
## Reference
|
|
246
254
|
|
|
247
255
|
- `.thrum/hotpath-gate.json` — the config file this skill generates
|
|
@@ -5,6 +5,8 @@ description: "Use after completing research, when updating research memory, when
|
|
|
5
5
|
|
|
6
6
|
# Researcher memory discipline
|
|
7
7
|
|
|
8
|
+
> **Before writing or editing any memory or state entry, use the `memory-current-rule` skill:** a memory states only the CURRENT rule (exceptions allowed), never a timeline of rulings, corrections or reversals, and every touch re-examines the whole memory.
|
|
9
|
+
|
|
8
10
|
You are the researcher. You write research findings, role-rules, and curate the
|
|
9
11
|
project-local research index. Common memory operations (write command shape,
|
|
10
12
|
body conventions, lookup patterns, edit/delete protocol) live in the common
|
|
@@ -203,6 +203,7 @@ thrum message read <msg-id> [<msg-id>...] # Mark as read
|
|
|
203
203
|
thrum message read --all # DRY RUN: list what --apply/--force would mark, marks nothing
|
|
204
204
|
thrum message read --all --apply # Mark the shown (already-seen) candidates
|
|
205
205
|
thrum message read --all --force # Mark ALL unread, seen or not (needs a fresh `--all` dry run first)
|
|
206
|
+
thrum message read --all --from @reviewer # DRY RUN restricted to one sender (also scopes --apply/--force)
|
|
206
207
|
thrum message search "<term>" # Full-text search across all messages (see § inbox above)
|
|
207
208
|
thrum message reindex # Rebuild the FTS index if search looks wrong
|
|
208
209
|
```
|
|
@@ -218,6 +219,7 @@ Subcommand flags:
|
|
|
218
219
|
--force Confirm deletion (required)
|
|
219
220
|
|
|
220
221
|
# message read
|
|
222
|
+
--from string Only read messages from this agent (accepts @agent or agent ID)
|
|
221
223
|
--all List seen+unread candidates (dry run); combine with --apply or --force to mark
|
|
222
224
|
--apply With --all: mark the shown (already-seen) candidates
|
|
223
225
|
--force With --all: mark ALL unread, seen or not (needs a fresh --all dry run first)
|
|
@@ -252,7 +254,7 @@ Flags:
|
|
|
252
254
|
--name string Human-readable agent name (defaults to role_hash)
|
|
253
255
|
--display string Display name for the agent
|
|
254
256
|
--intent string Initial work intent description
|
|
255
|
-
--runtime string Runtime preset: claude, codex, cursor,
|
|
257
|
+
--runtime string Runtime preset: claude, codex, cursor, opencode, auggie, cli-only
|
|
256
258
|
--preamble-file string Custom preamble file to compose with default preamble
|
|
257
259
|
--no-init Skip runtime config generation, just register agent
|
|
258
260
|
--force Force runtime config reinitialization; project instruction files are preserved
|
|
@@ -542,7 +544,7 @@ thrum init --skills --runtime cursor # Install skill to Cursor's skill
|
|
|
542
544
|
Flags:
|
|
543
545
|
|
|
544
546
|
```text
|
|
545
|
-
--runtime string Generate runtime-specific configs: claude|codex|cursor|
|
|
547
|
+
--runtime string Generate runtime-specific configs: claude|codex|cursor|opencode|cli-only|all
|
|
546
548
|
--skills Install thrum skill only (no MCP config, no startup script)
|
|
547
549
|
--stealth Use .git/info/exclude instead of .gitignore (zero footprint)
|
|
548
550
|
--force Force reinitialization. On a TTY this re-runs the wizard with existing values pre-seeded.
|
|
@@ -728,7 +730,7 @@ thrum runtime show claude --json
|
|
|
728
730
|
thrum runtime set-default <name> # Set the default runtime preset
|
|
729
731
|
```
|
|
730
732
|
|
|
731
|
-
Supported runtimes: `claude`, `codex`, `cursor`, `
|
|
733
|
+
Supported runtimes: `claude`, `codex`, `cursor`, `opencode`, `auggie`,
|
|
732
734
|
`cli-only`
|
|
733
735
|
|
|
734
736
|
---
|
|
@@ -835,7 +837,8 @@ thrum tmux connect # Attach (interactive picker)
|
|
|
835
837
|
thrum tmux connect <name> # Attach directly by name
|
|
836
838
|
thrum tmux restart <name> # Restart session with context snapshot
|
|
837
839
|
thrum tmux restart <name> # PREFERRED — re-applies the --model pin
|
|
838
|
-
thrum tmux restart <name> --force #
|
|
840
|
+
thrum tmux restart <name> --force # Skip the snapshot save prompt
|
|
841
|
+
thrum tmux restart <name> --resume # Resume the same pinned vendor session; skips Thrum snapshot
|
|
839
842
|
thrum tmux kill <name> # Tear down a session
|
|
840
843
|
thrum tmux queue <session> <command> # Submit command to session queue
|
|
841
844
|
thrum tmux queue <session> <command> --wait # Queue + block until complete
|
|
@@ -843,6 +846,11 @@ thrum tmux queue-status <session> # Show active + queued commands
|
|
|
843
846
|
thrum tmux cancel <command-id> # Cancel a queued or active command
|
|
844
847
|
```
|
|
845
848
|
|
|
849
|
+
Resume skips the Thrum snapshot phase. Registration must be proven before the
|
|
850
|
+
daemon submits one continue; the CLI reports `Resume continue: <outcome>` and
|
|
851
|
+
prints warnings when an unsupported effort argument is omitted. A staged
|
|
852
|
+
restart failure leaves the original session alive.
|
|
853
|
+
|
|
846
854
|
`tmux start` flags (operates on current working directory):
|
|
847
855
|
|
|
848
856
|
```text
|
|
@@ -858,7 +866,7 @@ thrum tmux cancel <command-id> # Cancel a queued or active comma
|
|
|
858
866
|
--role string Agent role (required unless --no-agent)
|
|
859
867
|
--module string Agent module (required unless --no-agent)
|
|
860
868
|
--intent string Initial work intent description
|
|
861
|
-
--runtime string Runtime preset: claude, codex, cursor,
|
|
869
|
+
--runtime string Runtime preset: claude, codex, cursor, opencode, auggie
|
|
862
870
|
--no-agent Skip agent registration (create session only)
|
|
863
871
|
--force Re-register even if agent exists; kill and recreate an existing session
|
|
864
872
|
```
|
|
@@ -917,7 +925,7 @@ thrum worktree list # List worktrees with agent
|
|
|
917
925
|
--role string Agent role
|
|
918
926
|
--module string Agent module
|
|
919
927
|
--intent string Initial work intent description
|
|
920
|
-
--runtime string Runtime preset: claude, codex, cursor,
|
|
928
|
+
--runtime string Runtime preset: claude, codex, cursor, opencode, auggie
|
|
921
929
|
```
|
|
922
930
|
|
|
923
931
|
`worktree teardown` flags:
|
|
@@ -97,20 +97,28 @@ No listener sub-agent, no polling, no CronCreate watchdog. Messages just arrive.
|
|
|
97
97
|
|
|
98
98
|
## Session Restart
|
|
99
99
|
|
|
100
|
-
When the context window is running low, restart
|
|
101
|
-
|
|
100
|
+
When the context window is running low, use a snapshot restart. If the vendor
|
|
101
|
+
login/session is broken but the vendor still has the conversation, use the
|
|
102
|
+
resume fallback instead:
|
|
102
103
|
|
|
103
104
|
- **Self-initiated:** Run `/thrum:restart` — saves snapshot, notifies
|
|
104
105
|
coordinator
|
|
105
|
-
- **Coordinator-initiated:** `thrum tmux restart <name>` —
|
|
106
|
-
`--force`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
- **Coordinator-initiated:** `thrum tmux restart <name>` — saves and restores a
|
|
107
|
+
Thrum snapshot. `--force` skips the graceful save prompt and snapshot flow.
|
|
108
|
+
- **Vendor-session recovery:** `thrum tmux restart <name> --resume` — skips the
|
|
109
|
+
Thrum snapshot phase and resumes the same pinned runtime's vendor session.
|
|
110
|
+
This is a fallback for a broken login/session, not a cross-runtime transfer.
|
|
111
|
+
It refuses `--force`, `--runtime`, and `--model`; the RPC also refuses
|
|
112
|
+
`provider`. Some runtimes discover the vendor ID by sending `/exit`, which can
|
|
113
|
+
close the current runtime process. A refusal after discovery may mean that
|
|
114
|
+
process already exited, so inspect the pane before retrying.
|
|
110
115
|
- **Automatic:** Configure `restart.auto_threshold` in `.thrum/config.json`
|
|
111
116
|
|
|
112
117
|
The snapshot is automatically included in `thrum prime` on the next session
|
|
113
|
-
start.
|
|
118
|
+
start. Resume skips this snapshot path; verify that the same vendor conversation
|
|
119
|
+
opened, ask for a small known-context response, and wait for visible activity.
|
|
120
|
+
`outcome_unknown` means the continue was accepted but its terminal state could
|
|
121
|
+
not be observed; inspect the pane and queue status before retrying.
|
|
114
122
|
|
|
115
123
|
## When Tmux Isn't Available
|
|
116
124
|
|
|
@@ -8,6 +8,8 @@ license: "Apache-2.0"
|
|
|
8
8
|
|
|
9
9
|
# thrum memory — capture and query structured memories
|
|
10
10
|
|
|
11
|
+
> **Before writing or editing any memory or state entry, use the `memory-current-rule` skill:** a memory states only the CURRENT rule (exceptions allowed), never a timeline of rulings, corrections or reversals, and every touch re-examines the whole memory.
|
|
12
|
+
|
|
11
13
|
## When to use thrum memory vs. other stores
|
|
12
14
|
|
|
13
15
|
Just learned something that future work should respect? → `thrum memory create`
|
|
@@ -71,7 +71,7 @@ Three distinct fields, do not conflate them:
|
|
|
71
71
|
| `thrum queue wait <bundle-id> [--reason "..."]` | -> waiting (alias for `queue update --status=waiting`; `--reason` optional) |
|
|
72
72
|
| `thrum queue ready <bundle-id>` | -> ready (alias for `queue update --status=ready`) |
|
|
73
73
|
| `thrum queue update <bundle-id> [flags]` | General partial update: `--status`, `--stage`, `--reason` (status_reason), `--title`, `--ref` (repeatable, replaces existing refs), `--priority`, `--assigned-by`, `--assigned-to-remove` - at least one required; `--status=blocked` requires `--reason` |
|
|
74
|
-
| `thrum queue drop <bundle-id>` | DELETE bundle + all its items, any status
|
|
74
|
+
| `thrum queue drop <bundle-id>` | DELETE bundle + all its items, any status. This is the normal close-out: drop a bundle in the same turn its work lands |
|
|
75
75
|
| `thrum queue assign <bundle-id> <agent-id>...` | Append agent(s) to assigned_to (routing metadata only) |
|
|
76
76
|
| `thrum queue note <bundle-id> --stdin` (or `--body-file <f>`) | Append a dated progress note (append-only; shown by `show`, last note previewed by `list`) |
|
|
77
77
|
| `thrum queue item add <bundle-id> --title "..."` | Add one embedded item |
|
|
@@ -143,9 +143,9 @@ thrum queue list --agent @impl_foo --status in_progress
|
|
|
143
143
|
**Finish vs discard:** `done` is a WAYPOINT, not a resting place - it marks a
|
|
144
144
|
bundle done and KEEPS it visible (`start` reopens it), but it is not where
|
|
145
145
|
finished work should live. `drop` DELETES it and every item inside, regardless
|
|
146
|
-
of status.
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
of status. When the work lands, `drop` it in the same turn (`done` first is
|
|
147
|
+
optional). A finished bundle is never kept: its history lives in git and the
|
|
148
|
+
bead, not in the queue.
|
|
149
149
|
|
|
150
150
|
**Close on a TRIGGER, not a reminder.** The moment a bundle's branch merges into
|
|
151
151
|
trunk, OR its last referenced bead closes, `done` + `drop` it in that same turn -
|
|
@@ -226,9 +226,8 @@ preference, not built here - out of scope per the project's security model.
|
|
|
226
226
|
it only tags your own bundle. Always follow it with a `thrum send`; the
|
|
227
227
|
recipient adds it themselves via `--from-message` (with that message's exact id).
|
|
228
228
|
- **Leaving done bundles to pile up.** `done` never removes anything - the
|
|
229
|
-
bundle stays, and only `drop` deletes it. Drop each
|
|
230
|
-
|
|
231
|
-
primary rot mode.
|
|
229
|
+
bundle stays, and only `drop` deletes it. Drop each bundle the turn its work
|
|
230
|
+
lands; a queue full of undropped done bundles is the primary rot mode.
|
|
232
231
|
- **Reaching for a `remove`/item-`drop` verb.** Removal (of a bundle and every
|
|
233
232
|
item inside it) happens only at the bundle level via `drop` - there is no
|
|
234
233
|
finer-grained delete. Renaming/re-titling is NOT in this category: `queue
|
|
@@ -5,6 +5,8 @@ description: "Use when reading or writing thrum state - checking your own agent-
|
|
|
5
5
|
|
|
6
6
|
# Using Thrum State
|
|
7
7
|
|
|
8
|
+
> **Before writing or editing any memory or state entry, use the `memory-current-rule` skill:** a memory states only the CURRENT rule (exceptions allowed), never a timeline of rulings, corrections or reversals, and every touch re-examines the whole memory.
|
|
9
|
+
|
|
8
10
|
## Overview
|
|
9
11
|
|
|
10
12
|
`thrum state` is a two-tier current-value store. **Agent-local** state
|
|
@@ -65,7 +65,7 @@ GOTCHAS
|
|
|
65
65
|
- --kill closes the ssh session only. Remote processes started in it (e.g. a tmux session) keep running.
|
|
66
66
|
- A dropped ssh loses any command running in it. Run long work under "tmux new -d" or nohup and re-run after a drop.
|
|
67
67
|
- A host that reports THIS machine's own hostname is refused (125); clear it with --kill.
|
|
68
|
-
- A live session keeps the TMUX_SSH_OPTS it started with;
|
|
68
|
+
- A live session keeps the TMUX_SSH_OPTS it started with; a different non-empty TMUX_SSH_OPTS only warns; run --kill first to apply it.
|
|
69
69
|
- --expect that never matched is reported as a warning after the command finishes.
|
|
70
70
|
|
|
71
71
|
EXIT CODES
|
|
@@ -325,12 +325,32 @@ write_ref() {
|
|
|
325
325
|
# Atomic: write to a temp file then rename, so a concurrent reader never sees
|
|
326
326
|
# a half-written ref (and a crash mid-write can't truncate the live file).
|
|
327
327
|
local tmp="${REF_FILE}.tmp.$$"
|
|
328
|
-
printf 'host=%s\nssh_target=%s\nsocket=%s\nsession=%s\nexpected_hostname=%s\nestablished_at=%s\n' \
|
|
328
|
+
printf 'host=%s\nssh_target=%s\nsocket=%s\nsession=%s\nexpected_hostname=%s\nestablished_at=%s\n%s' \
|
|
329
329
|
"$HOST" "$HOST" "$SOCKET" "$SESSION" "$EXPECTED_HOSTNAME" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
330
|
+
"$([ -n "$SSH_OPTS_UNKNOWN" ] || printf 'ssh_opts=%s\n' "$SSH_OPTS_STARTED")" \
|
|
330
331
|
> "$tmp"
|
|
331
332
|
mv -f "$tmp" "$REF_FILE"
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
# A live session keeps the TMUX_SSH_OPTS it was started with, so a caller whose non-empty TMUX_SSH_OPTS differ gets
|
|
336
|
+
# a warning (the run continues; --kill applies the new ones). A legacy ref with no ssh_opts= line is "unknown": warn only, and never record a guess for it.
|
|
337
|
+
SSH_OPTS_STARTED="${TMUX_SSH_OPTS:-}" # what write_ref records; becomes the recorded value while the session lives
|
|
338
|
+
SSH_OPTS_UNKNOWN=""
|
|
339
|
+
check_ssh_opts() {
|
|
340
|
+
[ -f "$REF_FILE" ] && has_session || return 0
|
|
341
|
+
local want rec; want=$(echo ${TMUX_SSH_OPTS:-})
|
|
342
|
+
if ! grep -q '^ssh_opts=' "$REF_FILE" 2>/dev/null; then
|
|
343
|
+
SSH_OPTS_UNKNOWN=1
|
|
344
|
+
[ -z "$want" ] || log "WARNING: this session predates ssh option recording, so the ssh options it runs with are unknown and may not be your TMUX_SSH_OPTS; to apply them run: $SCRIPT_NAME $HOST --kill"
|
|
345
|
+
return 0
|
|
346
|
+
fi
|
|
347
|
+
rec=$(grep '^ssh_opts=' "$REF_FILE" | head -1 | cut -d= -f2-)
|
|
348
|
+
SSH_OPTS_STARTED="$rec"
|
|
349
|
+
[ -z "$want" ] || [ "$(echo $rec)" = "$want" ] && return 0
|
|
350
|
+
log "WARNING: the session for $HOST was started with different ssh options ('${rec:-<none>}', not your '$want'); those stay in force. To apply your TMUX_SSH_OPTS run: $SCRIPT_NAME $HOST --kill"
|
|
351
|
+
return 0
|
|
352
|
+
}
|
|
353
|
+
|
|
334
354
|
# ---- tmux primitives -------------------------------------------------------
|
|
335
355
|
has_session() { tmux -L "$SOCKET" has-session -t "$STGT" 2>/dev/null; }
|
|
336
356
|
pane_cmd() { tmux -L "$SOCKET" list-panes -t "$PTGT" -F '#{pane_current_command}' 2>/dev/null | head -1; }
|
|
@@ -1145,6 +1165,7 @@ esac
|
|
|
1145
1165
|
mkdir -p "$REF_DIR"
|
|
1146
1166
|
acquire_lock "$LOCK_DIR"
|
|
1147
1167
|
refuse_if_guarded
|
|
1168
|
+
check_ssh_opts
|
|
1148
1169
|
read_watch || die "saved command watch is invalid; use --kill before starting another command" 125
|
|
1149
1170
|
if [ -n "$WATCH_NONCE" ]; then
|
|
1150
1171
|
die "a previous command is still being watched on $HOST -- NOT resent. Ways out: $SCRIPT_NAME $HOST --resume [--timeout N] (keep waiting) | $SCRIPT_NAME $HOST --abort (Ctrl-C it and clear the watch) | $SCRIPT_NAME $HOST --kill (drop the ssh session; remote processes keep running)" 125
|
|
@@ -95,8 +95,16 @@ a name — and deliver it as a message, not pane text:
|
|
|
95
95
|
[ESCALATION] <what needs a decision>: <why>. Action needed: <what>.
|
|
96
96
|
EOF
|
|
97
97
|
|
|
98
|
-
Writing the escalation only into your own pane is not delivery — nobody reads
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
Writing the escalation only into your own pane is not delivery — nobody reads watcher panes. Verify delivery
|
|
99
|
+
(`thrum sent --unread`) before assuming it was seen. Send it at most three times in total: the first send when you
|
|
100
|
+
find the condition, then a later send only on a later tick while the same condition is still open and unanswered, at
|
|
101
|
+
most once per tick and never three in one tick. Then stop: never escalate the same condition more than three times. If
|
|
102
|
+
three sends go unanswered, cancel the modal if one is still open, send your parent one closing line, and do not raise
|
|
103
|
+
the condition again unless it changes. SAFE recognized modals stay approver-of-record and are approved directly,
|
|
104
|
+
unchanged — this rule applies only to escalations that need a human decision.
|
|
105
|
+
|
|
106
|
+
This read-only archetype only reports: it never sends keys and cancels
|
|
107
|
+
nothing, so "cancel" above applies to the persistent watcher; here the cap
|
|
108
|
+
means three reports per condition. Count them with the snippet in
|
|
109
|
+
`persistent-watcher-archetype` (same key rule, one count file per condition,
|
|
110
|
+
unwatchable key `<agent>|unwatchable|<one-line reason>`).
|
|
@@ -9,7 +9,7 @@ import assert from "node:assert/strict";
|
|
|
9
9
|
import fs from "fs";
|
|
10
10
|
import os from "os";
|
|
11
11
|
import path from "path";
|
|
12
|
-
import { getConfigDir, installAssets } from "./installer.js";
|
|
12
|
+
import { getConfigDir, getSkillsSubdir, installAssets } from "./installer.js";
|
|
13
13
|
function stubCtx() {
|
|
14
14
|
return { client: { app: { log: async () => { } } } };
|
|
15
15
|
}
|
|
@@ -22,7 +22,10 @@ test("installAssets stages skills and commands; version gate skips reinstall", a
|
|
|
22
22
|
const cfg = path.join(tmp, "opencode");
|
|
23
23
|
assert.equal(getConfigDir(), cfg);
|
|
24
24
|
// protocol skills ride the wholesale skills copy (o4jgf item 5)
|
|
25
|
-
|
|
25
|
+
// open-core is flat (skills/<name>); the pro build nests under skills/<subdir>
|
|
26
|
+
const sub = getSkillsSubdir();
|
|
27
|
+
const skillsRoot = sub === "thrum" ? path.join(cfg, "skills") : path.join(cfg, "skills", sub);
|
|
28
|
+
const skillFile = path.join(skillsRoot, "snapshot-protocol", "SKILL.md");
|
|
26
29
|
assert.ok(fs.existsSync(skillFile));
|
|
27
30
|
assert.ok(fs.existsSync(path.join(cfg, "commands", "thrum-sleep.md")));
|
|
28
31
|
const mtimeBefore = fs.statSync(skillFile).mtimeMs;
|