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.
Files changed (31) hide show
  1. package/assets/commands/thrum-compact-extended.md +14 -12
  2. package/assets/commands/thrum-compact.md +17 -15
  3. package/assets/commands/thrum-prime.md +1 -0
  4. package/assets/skills/configure-roles/SKILL.md +0 -1
  5. package/assets/skills/coordinator-hotpath-merge-gate/SKILL.md +15 -6
  6. package/assets/skills/coordinator-maintaining-memory/SKILL.md +2 -0
  7. package/assets/skills/coordinator-merging-code/SKILL.md +25 -10
  8. package/assets/skills/coordinator-merging-code/resources/overbuild-check-prompt.md +60 -0
  9. package/assets/skills/coordinator-philosophy-merge-gate/SKILL.md +5 -4
  10. package/assets/skills/implementer-maintaining-memory/SKILL.md +2 -0
  11. package/assets/skills/memory-curate/SKILL.md +2 -0
  12. package/assets/skills/memory-current-rule/SKILL.md +121 -0
  13. package/assets/skills/memory-maintenance/SKILL.md +2 -0
  14. package/assets/skills/memory-write-discipline/SKILL.md +2 -0
  15. package/assets/skills/persistent-watcher-archetype/SKILL.md +142 -60
  16. package/assets/skills/project-hotpath-gate/SKILL.md +8 -0
  17. package/assets/skills/researcher-maintaining-memory/SKILL.md +2 -0
  18. package/assets/skills/thrum/resources/CLI_REFERENCE.md +14 -6
  19. package/assets/skills/thrum/resources/TMUX_SESSIONS.md +16 -8
  20. package/assets/skills/thrum-memory/SKILL.md +2 -0
  21. package/assets/skills/using-the-queue/SKILL.md +6 -7
  22. package/assets/skills/using-thrum-state/SKILL.md +2 -0
  23. package/assets/skills/using-tmux-ssh/resources/tmux-ssh +23 -2
  24. package/assets/skills/watcher-archetype/SKILL.md +13 -5
  25. package/dist/installer-assets.test.js +5 -2
  26. package/dist/installer.d.ts +5 -1
  27. package/dist/installer.js +38 -7
  28. package/dist/installer.test.js +53 -0
  29. package/package.json +4 -1
  30. package/dist/permission-fixture.integration.test.js +0 -506
  31. /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 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).
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. Otherwise escalate to your `parent` with a one-line reason.
66
- - Judge the TARGET, not the keyword: `rm -rf /tmp/<dir the agent just
67
- created>` is routine cleanup; `rm -rf /tmp/*` is not. One `rm` of a stale
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 an escalate.
70
- - Escalating is always allowed. Refusing only because a keyword appeared is
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. `thrum tmux send <agent>
96
- --keys 1`. Named keys (`Enter`, `Escape`, `Up`, `Down`, `Left`, `Right`,
97
- `Tab`, digits) deliver in order, comma-separated or repeated
98
- (`--keys Down,Enter` or `--keys Down --keys Enter`) — confirm the exact
99
- syntax with `thrum tmux send --help` if in doubt, never guess it.
100
- - To dismiss without selecting → `thrum tmux send <agent> --keys Escape`.
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 (`rpcrouter: caller-peer lacks
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
- `resources/thrum-capture-fallback.sh <capture|key> <agent> <ssh_target>
128
- <ssh_user> <repo_path> [keys...]`, which is the piece that actually
129
- runs `ssh "<ssh_user>@<ssh_target>" "cd '<repo_path>' && thrum tmux
130
- send '<agent>' --keys <keys>"` — `thrum-capture-fallback.sh` does no
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 `ssh
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
- Only escalate to your `parent` after exhausting this ladder, or
142
- immediately on an UNRECOGNIZED modal — and when you do, include the fresh
143
- capture.
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
- - **Deny, with feedback** → `--keys 3` (or `--keys Escape` on the two-option
163
- Read-prompt variant, which has no "No, and tell Claude" line).
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 on anything you
210
- don't understand, any irreversible/high-blast-radius action (agent
211
- delete/retire/teardown, messages.db, the shared binary, daemon restart,
212
- anything outside `/tmp`), and any stuck/wedged state you can't safely
213
- resolve. The tree is REFERENCE DATA — who to notify — not an active
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
- watcher panes. If you get no response within 15 minutes, resend and verify
242
- delivery (`thrum sent --unread`) before assuming it was seen. SAFE recognized
243
- modals stay approver-of-record and are approved directly, unchanged — this
244
- rule applies only to escalations that need a human decision.
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: escalate every cycle until it is removed
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, mark dead, or fall silent on. It stays in your roster and it stays your
332
+ drop or mark dead. It stays in your roster and it stays your
252
333
  duty.
253
334
 
254
- - **Escalate it to your `parent` every cycle it remains unwatchable —
255
- repeatedly, not once.** A single "fully isolated" notice that you then stop
256
- repeating is exactly the silent-give-up this archetype exists to prevent:
257
- the roster member goes dark with nobody positioned to notice. Re-send the
258
- escalation on each tick it is still unwatchable. (This is a standing
259
- per-cycle duty, unlike the one-shot declared-intent nudge.)
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`, you keep escalating.
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, keep
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
- `"New message from @<you> -- run \`thrum inbox --unread\`..."` line,
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** — `resources/thrum-watch-
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 `thrum tmux
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, `"ROSTER
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, gemini, opencode, auggie, cli-only
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|gemini|opencode|cli-only|all
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`, `gemini`, `opencode`, `auggie`,
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 # Skips confirmation AND DROPS THE --model PIN
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, gemini, opencode, auggie
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, gemini, opencode, auggie
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 with a conversation snapshot
101
- preserved:
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>` — **PLAIN, not `--force`.**
106
- `--force` **drops the `--model` pin**; a plain restart re-applies it at the
107
- readiness probe. Only use `--force` if a plain restart does not take, and then
108
- re-pin and verify the model **off the pane** (`runtime-config get` reports what
109
- was requested, not what is running).
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 (the one destructive verb - drop promptly once a done bundle's record stops being useful) |
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. Reconcile at session start and natural breakpoints: `done` then a
147
- prompt `drop` is the normal close-out; `drop` alone is fine too. Don't let a
148
- done bundle outlive your next reconcile unless you deliberately keep it.
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 done bundle promptly once
230
- its record stops being useful; a queue full of undropped done bundles is the
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; to change them run --kill first.
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
- watcher panes. If you get no response within 15 minutes, resend and verify
100
- delivery (`thrum sent --unread`) before assuming it was seen. SAFE recognized
101
- modals stay approver-of-record and are approved directly, unchanged — this
102
- rule applies only to escalations that need a human decision.
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
- const skillFile = path.join(cfg, "skills", "snapshot-protocol", "SKILL.md");
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;