opencode-thrum-pro 0.1.5 → 0.1.6

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 (67) hide show
  1. package/assets/commands/thrum-_compact-runtime-commands.md +61 -0
  2. package/assets/commands/thrum-_snapshot-protocol.md +17 -6
  3. package/assets/commands/thrum-_stamp-protocol.md +9 -2
  4. package/assets/commands/thrum-compact-extended.md +352 -0
  5. package/assets/commands/thrum-compact.md +300 -0
  6. package/assets/commands/thrum-inbox.md +1 -1
  7. package/assets/commands/thrum-restart-extended.md +84 -11
  8. package/assets/commands/thrum-restart.md +105 -13
  9. package/assets/commands/thrum-send.md +4 -1
  10. package/assets/commands/thrum-sleep-extended.md +67 -46
  11. package/assets/commands/thrum-sleep.md +67 -58
  12. package/assets/skills/adversarial-critique/SKILL.md +5 -8
  13. package/assets/skills/brainstorm-steward/SKILL.md +65 -11
  14. package/assets/skills/brainstormer-staying-current/SKILL.md +1 -4
  15. package/assets/skills/choosing-subagent-models/SKILL.md +14 -0
  16. package/assets/skills/conversation-only-context/SKILL.md +11 -11
  17. package/assets/skills/conversation-only-context/resources/driver-subagent-pattern.md +1 -1
  18. package/assets/skills/conversation-only-context/resources/remote-environment-scenarios.md +1 -1
  19. package/assets/skills/coordinator-assessing-agent-completion/SKILL.md +98 -6
  20. package/assets/skills/coordinator-context-monitoring/SKILL.md +29 -19
  21. package/assets/skills/coordinator-deploying-a-node/SKILL.md +124 -0
  22. package/assets/skills/coordinator-dispatching-work/SKILL.md +24 -11
  23. package/assets/skills/coordinator-hotpath-merge-gate/SKILL.md +62 -20
  24. package/assets/skills/coordinator-managing-state-and-lifecycle/SKILL.md +6 -6
  25. package/assets/skills/coordinator-merging-code/SKILL.md +25 -11
  26. package/assets/skills/coordinator-philosophy-merge-gate/SKILL.md +28 -7
  27. package/assets/skills/coordinator-plan-reconcile-gate/SKILL.md +3 -3
  28. package/assets/skills/coordinator-rolling-out-to-nodes/SKILL.md +92 -0
  29. package/assets/skills/coordinator-running-autonomous-feature-development/SKILL.md +3 -3
  30. package/assets/skills/coordinator-running-brainstorm-cycles/SKILL.md +21 -20
  31. package/assets/skills/coordinator-running-review-cycles/SKILL.md +15 -4
  32. package/assets/skills/diagnosing-fleet-and-daemon/SKILL.md +0 -2
  33. package/assets/skills/efficient-multi-agent-research/SKILL.md +3 -3
  34. package/assets/skills/implementer-orchestrating-execution/SKILL.md +11 -14
  35. package/assets/skills/orchestrate/SKILL.md +47 -35
  36. package/assets/skills/persistent-watcher-archetype/SKILL.md +172 -25
  37. package/assets/skills/persistent-watcher-archetype/resources/capture_archive.py +618 -0
  38. package/assets/skills/persistent-watcher-archetype/resources/jev_watcher_filter.py +837 -0
  39. package/assets/skills/persistent-watcher-archetype/resources/roster-watch.sh +1140 -0
  40. package/assets/skills/persistent-watcher-archetype/resources/roster-watch_test.sh +291 -0
  41. package/assets/skills/persistent-watcher-archetype/resources/thrum-capture-fallback.sh +2 -2
  42. package/assets/skills/persistent-watcher-archetype/resources/thrum-watch-pane-capture.sh +20 -8
  43. package/assets/skills/prime-agent/SKILL.md +10 -0
  44. package/assets/skills/project-setup/SKILL.md +22 -16
  45. package/assets/skills/project-setup/resources/implementation-agent.md +56 -35
  46. package/assets/skills/researcher-investigating/SKILL.md +2 -5
  47. package/assets/skills/thrum/SKILL.md +14 -7
  48. package/assets/skills/thrum/resources/ANTI_PATTERNS.md +16 -18
  49. package/assets/skills/thrum/resources/CLI_REFERENCE.md +17 -11
  50. package/assets/skills/thrum/resources/LISTENER_PATTERN.md +9 -3
  51. package/assets/skills/thrum/resources/MESSAGING.md +18 -11
  52. package/assets/skills/thrum/resources/WORKTREES.md +1 -1
  53. package/assets/skills/thrum-memory/SKILL.md +1 -1
  54. package/assets/skills/trace-spec-to-plan/resources/sweep-agent-prompt.md +1 -1
  55. package/assets/skills/update-queue/SKILL.md +6 -8
  56. package/assets/skills/using-the-dashboard/SKILL.md +30 -0
  57. package/assets/skills/using-the-queue/SKILL.md +76 -5
  58. package/assets/skills/using-thrum-state/SKILL.md +83 -0
  59. package/assets/skills/verify-against-plan/SKILL.md +120 -5
  60. package/assets/skills/watcher-archetype/SKILL.md +18 -0
  61. package/assets/skills/writing-durable-markdown/SKILL.md +5 -0
  62. package/dist/installer.js +4 -4
  63. package/dist/permission-fixture.integration.test.js +4 -4
  64. package/package.json +1 -1
  65. package/assets/skills/coordinator-deploying-the-fleet/SKILL.md +0 -396
  66. package/dist/allowlist-sync.test.d.ts +0 -1
  67. package/dist/allowlist-sync.test.js +0 -44
@@ -0,0 +1,300 @@
1
+ ---
2
+ description:
3
+ Stage a standard 11-section snapshot, verify it landed, then send /compact to
4
+ this agent's OWN tmux pane so it stays alive (subagents, loops, crons survive)
5
+ and resumes at low context. Use for in-place context relief when you do NOT
6
+ want to kill the session.
7
+ ---
8
+
9
+ # Compact — Snapshot Then Compact In Place
10
+
11
+ Compose a standard 11-section prose continuation, write it directly to your
12
+ restart file, verify it landed, then send `/compact` to your own tmux pane.
13
+ Unlike `/thrum:restart` and `/thrum:sleep`, compact does NOT end your session and
14
+ does NOT kill your tmux pane — the agent stays ALIVE. Any sub-agents, background
15
+ loops, and cron schedules you own keep running across the compaction (confirmed
16
+ empirically). After compaction you Read your snapshot by hand and re-prime lean.
17
+
18
+ ## When to Use
19
+
20
+ - Your context is high and you want relief WITHOUT losing the live session —
21
+ you own sub-agents, a background loop, or a cron that must survive.
22
+ - You are mid-task and a fresh session (which loses in-flight sub-agent handles
23
+ and scheduler state) would be more expensive than a compaction.
24
+
25
+ For context-exhaustion or stuck-state cases where a FRESH session is the right
26
+ answer — or where the coordinator should move you — use `/thrum:restart`. For
27
+ operator-shutdown parking that kills the session, use `/thrum:sleep`. Compact is
28
+ the only one of the three that keeps the runtime process alive.
29
+
30
+ ## Steps
31
+
32
+ ### 1. Verify tmux session (Tier 1 pre-check; run BEFORE composing anything)
33
+
34
+ ```bash
35
+ # compact SENDS /compact to your own pane, so a non-tmux agent has no pane to
36
+ # target. Fail fast before you spend a snapshot composing.
37
+ SESSION_RAW=$(thrum whoami --field tmux_session)
38
+ if [ -z "$SESSION_RAW" ]; then
39
+ echo "ERROR: the compact command requires a tmux-managed agent session (tmux_session field is empty)."
40
+ echo "For a non-tmux agent, run /compact yourself and Read your snapshot by hand afterward."
41
+ exit 1
42
+ fi
43
+ ```
44
+
45
+ If `tmux_session` is empty: ABORT before composing. Exit code 1.
46
+
47
+ ### 2. Read the shared snapshot-composition partial
48
+
49
+ **First, persist state + queue.** Update your personal state
50
+ (`thrum state set --kind personal_state ...` — see the `using-thrum-state`
51
+ skill) and your committed work (`thrum queue` — see the `using-the-queue`
52
+ skill) BEFORE composing your snapshot below — these survive compaction
53
+ independently of the prose continuation file and are what a post-compact
54
+ render reads back.
55
+
56
+ Read the partial at the absolute path (resolve `$REPO` yourself first via
57
+ `thrum agent worktree --authoritative` — NEVER `thrum whoami --field worktree`
58
+ or `git rev-parse --show-toplevel`, both cwd-resolved and the root cause of
59
+ a documented snapshot-save loss class; there is no git fallback):
60
+
61
+ ```text
62
+ ${REPO}/claude-plugin/commands/_snapshot-protocol.md
63
+ ```
64
+
65
+ Apply its Step 2 (compose your continuation) per the structure guidance.
66
+
67
+ **Use the STANDARD 11-section structure.** For comprehensive
68
+ designer/architect-grade snapshots, use `/thrum:compact-extended` instead.
69
+
70
+ **Note on §1 framing:** For a compact snapshot, §1 frames as "where work stands
71
+ at compaction" — you are continuing in place, not completing and not parking.
72
+
73
+ **Your snapshot's FIRST line MUST be the compact resume header**, above §1. It
74
+ survives even if post-compact orientation fails, so it is read-first and
75
+ self-contained:
76
+
77
+ > 🔴 ON RESUME — you were COMPACTED, not restarted. Daemon binding is intact
78
+ > (subagents/loops/crons survived). Resume LEAN: this file is delivered inline in the injected briefing (read it FIRST). Expect a
79
+ > `thrum prime --light` briefing already auto-injected by the SessionStart
80
+ > hook; only run `thrum:prime-agent` if it did NOT appear. Do NOT manually run
81
+ > full `thrum prime`. Then continue from §9.
82
+
83
+ Then §1…§11 as normal, with §9 (Numbered resume plan) actionable from a
84
+ compacted (not cold) start.
85
+
86
+ ### 3. Write the continuation
87
+
88
+ First run this PREP block (it mints the path and a per-run nonce), then use the
89
+ Write tool per Step 3 of the partial:
90
+
91
+ ```bash
92
+ # The Bash tool does NOT persist shell state across calls, so this block
93
+ # re-resolves identity itself. Fail closed: no cwd/git fallback.
94
+ REPO=$(
95
+ thrum agent worktree --authoritative 2>/dev/null
96
+ ) || { echo "ERROR: cannot authoritatively resolve your worktree via the daemon. Refusing to fall back to cwd/git-toplevel — that would risk silently saving this snapshot to the wrong repo. Check daemon connectivity and retry."; exit 1; }
97
+ [ -n "$REPO" ] || { echo "ERROR: thrum agent worktree --authoritative returned empty"; exit 1; }
98
+ AGENT=$(thrum whoami --field agent_id) || { echo "ERROR: agent not registered"; exit 1; }
99
+ [ -n "$AGENT" ] || { echo "ERROR: empty agent_id"; exit 1; }
100
+ mkdir -p "${REPO}/.thrum/restart"
101
+ SNAPSHOT="${REPO}/.thrum/restart/${AGENT}.md"
102
+ NONCE="zn6fx-$(date +%s)-$$-${RANDOM}${RANDOM}"
103
+ echo "SNAPSHOT_PATH=${SNAPSHOT}"
104
+ echo "SNAPSHOT_NONCE=${NONCE}"
105
+ ```
106
+
107
+ Write to EXACTLY the printed `SNAPSHOT_PATH` (not a path you re-derive), and make
108
+ sure the file contains the marker below (as its final line), with the printed `SNAPSHOT_NONCE` — verify searches the whole file for it:
109
+
110
+ ```text
111
+ <!-- snapshot-nonce: <SNAPSHOT_NONCE> -->
112
+ ```
113
+
114
+ **Record both values** — the verify step pastes them into its verify block, which
115
+ fails closed unless the file at that exact path carries that nonce.
116
+
117
+ This file lives on disk and is UNAFFECTED by compaction — compaction rewrites
118
+ your conversation context, never your worktree. So the snapshot you write now is
119
+ readable after `/compact` fires. There is no durable-backup step (the worktree is
120
+ not torn down, so `/thrum:sleep`'s teardown-loss risk does not apply here).
121
+
122
+ ### 4. Verify your snapshot, then fire /compact (ONE block)
123
+
124
+ > ✅ **Use the SINGLE-call self-send form below — it is required, not stylistic.**
125
+ > `thrum tmux send` (OD-5 = B; supersedes `thrum tmux key`,
126
+ > which is being retired) takes exactly ONE client call: the daemon
127
+ > types the text, waits a settle gap, checks for a pending dialog, and only then
128
+ > sends the confirming Enter — all server-side, in that order, within the SAME
129
+ > call. There is no separate `key --type` + `key Enter` pair to race, because
130
+ > there is no second client call: a client-side race is structurally impossible
131
+ > here, not merely avoided by convention.
132
+ > 🔴 **Never issue two separate calls (one for the text, one for Enter)** — that
133
+ > shape is what the retired `tmux key` two-step form did, and it is what
134
+ > produced a truncated `/compac` → invalid command → silent no-op (measured).
135
+ > A single `thrum tmux send "$SESSION" "<command>"` call has no such split to
136
+ > make.
137
+
138
+ On a non-Claude runtime this block sends that runtime's own compact-equivalent
139
+ command in place of `/compact` — see
140
+ `${REPO}/claude-plugin/commands/_compact-runtime-commands.md` for the cited
141
+ per-runtime table; `scripts/sync-skills.sh` applies the substitution when
142
+ syncing this file to each runtime's plugin tree.
143
+
144
+ Firing `/compact` before the Step 3 Write lands compacts a pre-write context and
145
+ resumes from a stale/blank file — the exact failure this command prevents. This
146
+ block (paste the two values printed by the Step 3 PREP block into its first two
147
+ lines) re-resolves identity (the Bash tool does NOT persist shell state across
148
+ calls, so earlier steps' variables are gone), verifies the snapshot on disk, and
149
+ only then fires `/compact` — all in ONE call so nothing depends on a prior block.
150
+
151
+ ```bash
152
+ SNAPSHOT="<PASTE SNAPSHOT_PATH FROM STEP 3>"
153
+ NONCE="<PASTE SNAPSHOT_NONCE FROM STEP 3>"
154
+ # Fresh authoritative re-resolution; must agree with the path you actually wrote.
155
+ REPO=$(
156
+ thrum agent worktree --authoritative 2>/dev/null
157
+ ) || { echo "ERROR: cannot authoritatively resolve your worktree via the daemon. Refusing to fall back to cwd/git-toplevel — that would risk silently resuming from the wrong repo's snapshot. Check daemon connectivity and retry."; exit 1; }
158
+ [ -n "$REPO" ] || { echo "ERROR: thrum agent worktree --authoritative returned empty"; exit 1; }
159
+ AGENT=$(thrum whoami --field agent_id) || { echo "ERROR: agent not registered"; exit 1; }
160
+ [ -n "$AGENT" ] || { echo "ERROR: empty agent_id"; exit 1; }
161
+ SESSION_RAW=$(thrum whoami --field tmux_session)
162
+ SESSION=${SESSION_RAW%%:*}
163
+ EXPECTED="${REPO}/.thrum/restart/${AGENT}.md"
164
+ BIND_OK=1
165
+ if [ -z "$SNAPSHOT" ] || [ -z "$NONCE" ] || [[ "$SNAPSHOT$NONCE" == *"<PASTE"* ]]; then
166
+ echo "VERIFY FAILED: SNAPSHOT/NONCE not filled in — paste SNAPSHOT_PATH and SNAPSHOT_NONCE from Step 3. SNAPSHOT_OK=0"; BIND_OK=0
167
+ elif [ "$SNAPSHOT" != "$EXPECTED" ]; then
168
+ echo "VERIFY FAILED: identity/worktree DRIFT between write time and verify time. Written path: $SNAPSHOT — re-resolved path: $EXPECTED. SNAPSHOT_OK=0"; BIND_OK=0
169
+ fi
170
+
171
+ # mtime: choose the stat dialect explicitly. GNU `stat -f` means --file-system
172
+ # (wrong, multiline) — the `stat -f %m . || stat -c %Y` one-liner is fragile on
173
+ # Linux, so branch on which dialect this box speaks.
174
+ if stat -f %m . >/dev/null 2>&1; then
175
+ MTIME=$(
176
+ stat -f %m "$SNAPSHOT" 2>/dev/null
177
+ )
178
+ else
179
+ MTIME=$(
180
+ stat -c %Y "$SNAPSHOT" 2>/dev/null
181
+ )
182
+ fi
183
+ NOW=$(date +%s)
184
+ AGE=$(( NOW - ${MTIME:-0} ))
185
+
186
+ SNAPSHOT_OK=1
187
+ if [ "$BIND_OK" = 0 ]; then
188
+ SNAPSHOT_OK=0 # binding failure already reported above
189
+ elif [ ! -s "$SNAPSHOT" ] || [ ! -r "$SNAPSHOT" ]; then
190
+ echo "VERIFY FAILED: snapshot missing/empty/unreadable at $SNAPSHOT"; SNAPSHOT_OK=0
191
+ elif [ "$AGE" -gt 300 ]; then
192
+ echo "VERIFY FAILED: snapshot is ${AGE}s old — stale (prior write), not this session's"; SNAPSHOT_OK=0
193
+ elif ! grep -qF -- "$NONCE" "$SNAPSHOT"; then
194
+ echo "VERIFY FAILED: nonce not found in $SNAPSHOT — file at that path is not this session's write"; SNAPSHOT_OK=0
195
+ else
196
+ echo "VERIFY OK: snapshot non-empty, readable, written ${AGE}s ago"
197
+ fi
198
+
199
+ SESSION_OK=1
200
+ if [ -n "$SESSION" ] && tmux has-session -t "$SESSION" 2>/dev/null; then
201
+ echo "VERIFY OK: tmux session '$SESSION' resolves"
202
+ else
203
+ echo "VERIFY FAILED: tmux session '$SESSION' (from '$SESSION_RAW') does not resolve"; SESSION_OK=0
204
+ fi
205
+
206
+ if [ "$SNAPSHOT_OK" = 1 ] && [ "$SESSION_OK" = 1 ]; then
207
+ # ONE call: `thrum tmux send` queues this text+Enter delivery on the daemon
208
+ # (internal/daemon/rpc.HandleQueue -> sendQueuedCommand). The daemon waits
209
+ # for your own pane to go idle (this turn ending is what makes it idle —
210
+ # emit nothing further after this call), types the command, pauses a settle
211
+ # gap, checks the pane for a pending dialog, and ONLY THEN sends Enter. If a
212
+ # dialog is showing at that moment, the daemon withholds Enter itself
213
+ # (queuedPaneShowsDialogFn / ErrPaneShowsDialog) — a HOLD happens
214
+ # server-side, automatically, with no separate client-side check needed
215
+ # here. There is no second client call for Enter, so there is nothing to
216
+ # race: the old two-step `key --type` + `key Enter` truncation class
217
+ # (measured: a timed-out type left a separate Enter to submit "/compac")
218
+ # cannot occur when text+Enter are ONE call.
219
+ #
220
+ # This dispatch is ASYNC, not instant: it typically fires within a few
221
+ # seconds of your pane going idle, not in the same instant this call
222
+ # returns. That is expected — you are not emitting anything further this
223
+ # turn regardless, so the delay costs nothing.
224
+ thrum tmux send "$SESSION" "/compact"
225
+ SEND_RC=$?
226
+ # Exit 3 = HELD (Enter withheld), 4 = VERDICT UNKNOWN: "/compact" is already
227
+ # typed in the pane. Do NOT retype it (that yields "/compact/compact"); report
228
+ # and stop so a human/coordinator can inspect the pane.
229
+ if [ "$SEND_RC" -eq 3 ] || [ "$SEND_RC" -eq 4 ]; then
230
+ echo "HOLDING: thrum tmux send exited ${SEND_RC} (3=Enter WITHHELD, 4=verdict unknown) — /compact is typed but not confirmed submitted. NOT retrying."
231
+ echo "Report this to your coordinator (or the operator if you are top-level) and stop."
232
+ exit 1
233
+ elif [ "$SEND_RC" -ne 0 ]; then
234
+ echo "ERROR: thrum tmux send exited ${SEND_RC} — /compact was not queued. Report and stop."
235
+ exit 1
236
+ fi
237
+ # thrum-xyz: queue the RESUME PROMPT right behind /compact, on the same
238
+ # daemon queue. Hook output is context only — it does not start a turn — so
239
+ # without this an idle pane with an empty inbox can sit at 0% context, empty
240
+ # composer, until a human types. The daemon dispatches it once the pane is
241
+ # idle again AFTER the compaction (FIFO behind /compact), so it is not a
242
+ # timer and it fires exactly once.
243
+ RESUME_PROMPT="Compaction complete - please continue: your Resume Plan is in the auto-injected briefing (# Previous Session Context). If it is missing, read ${SNAPSHOT} or the newest *-restart.md in .thrum/agents/${AGENT}/sessions/ of the main repo, then execute its numbered resume plan."
244
+ thrum tmux send "$SESSION" "$RESUME_PROMPT"
245
+ RESUME_RC=$?
246
+ if [ "$RESUME_RC" -eq 0 ]; then
247
+ :
248
+ else
249
+ echo "WARNING: thrum tmux send exited ${RESUME_RC} — the post-compact resume prompt was NOT queued (exit 3/4 = already typed, do NOT retype). If the pane sits idle after compaction, send it a resume prompt by hand."
250
+ fi
251
+ else
252
+ echo "HOLDING: not firing /compact. Report the VERIFY FAILED line(s) above to"
253
+ echo "your coordinator (or the operator if you are top-level) and stop."
254
+ exit 1
255
+ fi
256
+ ```
257
+
258
+ **Fail-closed binding rule:** the block only passes for the exact path and nonce
259
+ the Step 3 PREP block printed. It refuses (VERIFY FAILED, no `/compact`) if the
260
+ values are empty or still placeholders, if a fresh re-resolution of your identity
261
+ yields a different path (drift), or if the file at that path lacks the nonce — a
262
+ fresh file merely sitting at the re-derived path is NOT proof you wrote it.
263
+
264
+ If EITHER check fails, the block HOLDS and does not fire `/compact` — holding is
265
+ safe; compacting against an unverified snapshot loses the context this command
266
+ exists to preserve. After a successful send, the turn ends; emit no further tool
267
+ calls or prose this turn. The send is queued, not synchronous — it fires once
268
+ your pane goes idle, which only happens if you stop acting now.
269
+
270
+ ## How resume works
271
+
272
+ After `/compact` the SAME process and session survive — the daemon binding is
273
+ intact (that is why your sub-agents, loops, and crons kept running). Full
274
+ `thrum prime` rebuilds identity + binding + full briefing for a NEW session; none
275
+ of that is needed here. Resume LEAN, in this order:
276
+
277
+ 1. **Your Resume Plan arrives inline — do not go looking for the file.** The
278
+ `SessionStart` hook auto-injects a `thrum prime --light` briefing zero-turn,
279
+ and its `# Previous Session Context` section carries your full snapshot,
280
+ including the `ON RESUME` header; execute §9 from it. That prime ARCHIVES the
281
+ snapshot (moves `${REPO}/.thrum/restart/${AGENT}.md` into the main repo's
282
+ `.thrum/agents/${AGENT}/sessions/`), so the original path is normally GONE
283
+ by the time you look — that is by design, not loss. Only if no briefing
284
+ appeared (daemon unreachable), read the newest `*-restart.md` in that
285
+ `sessions/` directory (or `${REPO}/.thrum/restart/${AGENT}.md` if it still
286
+ exists) and run `thrum:prime-agent` manually as the fallback. Never manually
287
+ run full `thrum prime` when the briefing is present.
288
+ 2. **A resume prompt starts your first turn.** Step 4 queued a "Compaction
289
+ complete - please continue" prompt on the daemon right behind `/compact`; it
290
+ is delivered once the pane is idle after compaction, so a turn starts even
291
+ with an empty inbox.
292
+ 3. **Continue** from §9. Reconnect to your still-live sub-agents, loops, and
293
+ crons rather than re-dispatching.
294
+
295
+ Runtime-specific compaction-recovery hooks (if this runtime has any) are
296
+ documented in that runtime's own plugin tree (its hooks manifest and
297
+ the scripts it points to), not here.
298
+
299
+ **Follow the Resume Plan in your injected `# Previous Session Context` (snapshot
300
+ saved at `${REPO}/.thrum/restart/${AGENT}.md`, archived by the prime) post-compact.**
@@ -9,7 +9,7 @@ thrum inbox # All recent messages (auto-marks as read)
9
9
  thrum inbox --unread # Unread only (does not mark as read)
10
10
  thrum inbox --json # Machine-readable
11
11
  thrum sent --unread # Check sent items with unread recipients
12
- thrum message read --all # Mark all messages as read
12
+ thrum message read --all --force # Mark ALL unread as read (run `read --all` alone first — dry run)
13
13
  ```
14
14
 
15
15
  **Search — do not page through it.** The default page is 10, newest-first, so
@@ -34,12 +34,16 @@ artifact alone.
34
34
 
35
35
  ```bash
36
36
  # $REPO must be YOUR worktree — the directory `thrum prime` reads the restart
37
- # snapshot back from. Resolve it from the daemon's authoritative identity, NOT
38
- # `git rev-parse` (which keys off the current shell CWD and would write to the
39
- # wrong .thrum/restart/ if a bash step left your worktree). Fall back to git
40
- # only if whoami can't answer.
41
- REPO=$(thrum whoami --field worktree 2>/dev/null)
42
- [ -n "$REPO" ] || REPO=$(git rev-parse --show-toplevel) || { echo "ERROR: cannot resolve your worktree"; exit 1; }
37
+ # snapshot back from. Resolve it ONLY via the daemon's authoritative,
38
+ # peercred-resolved identity (`thrum agent worktree --authoritative`) — NEVER
39
+ # via `thrum whoami --field worktree` or `git rev-parse --show-toplevel`
40
+ # (both cwd-resolved). A cwd-derived pattern could silently
41
+ # resolve to a DIFFERENT agent's worktree, or the main repo. There is NO git
42
+ # fallback — if the daemon can't answer, refuse and stop rather than guess.
43
+ REPO=$(
44
+ thrum agent worktree --authoritative 2>/dev/null
45
+ ) || { echo "ERROR: cannot authoritatively resolve your worktree via the daemon. Refusing to fall back to cwd/git-toplevel — that would risk silently saving this snapshot to the wrong repo. Check daemon connectivity and retry."; exit 1; }
46
+ [ -n "$REPO" ] || { echo "ERROR: thrum agent worktree --authoritative returned empty"; exit 1; }
43
47
  AGENT=$(thrum whoami --field agent_id) || { echo "ERROR: agent not registered"; exit 1; }
44
48
  [ -n "$AGENT" ] || { echo "ERROR: empty agent_id"; exit 1; }
45
49
  mkdir -p "${REPO}/.thrum/restart"
@@ -64,8 +68,36 @@ structure and the EXTENDED 16-section structure with per-section guidance.
64
68
 
65
69
  ### 3. Write the continuation
66
70
 
67
- Per Step 3 of the partial, use the Write tool to save your composed continuation
68
- to `${REPO}/.thrum/restart/${AGENT}.md`. `thrum prime` auto-injects this file on
71
+ First run this PREP block (it mints the path and a per-run nonce):
72
+
73
+ ```bash
74
+ # The Bash tool does NOT persist shell state across calls, so this block
75
+ # re-resolves identity itself. Fail closed: no cwd/git fallback.
76
+ REPO=$(
77
+ thrum agent worktree --authoritative 2>/dev/null
78
+ ) || { echo "ERROR: cannot authoritatively resolve your worktree via the daemon. Refusing to fall back to cwd/git-toplevel — that would risk silently saving this snapshot to the wrong repo. Check daemon connectivity and retry."; exit 1; }
79
+ [ -n "$REPO" ] || { echo "ERROR: thrum agent worktree --authoritative returned empty"; exit 1; }
80
+ AGENT=$(thrum whoami --field agent_id) || { echo "ERROR: agent not registered"; exit 1; }
81
+ [ -n "$AGENT" ] || { echo "ERROR: empty agent_id"; exit 1; }
82
+ mkdir -p "${REPO}/.thrum/restart"
83
+ SNAPSHOT="${REPO}/.thrum/restart/${AGENT}.md"
84
+ NONCE="zn6fx-$(date +%s)-$$-${RANDOM}${RANDOM}"
85
+ echo "SNAPSHOT_PATH=${SNAPSHOT}"
86
+ echo "SNAPSHOT_NONCE=${NONCE}"
87
+ ```
88
+
89
+ Write to EXACTLY the printed `SNAPSHOT_PATH` (not a path you re-derive), and make
90
+ sure the file contains the marker below (as its final line), with the printed `SNAPSHOT_NONCE` — verify searches the whole file for it:
91
+
92
+ ```text
93
+ <!-- snapshot-nonce: <SNAPSHOT_NONCE> -->
94
+ ```
95
+
96
+ **Record both values** — the verify step pastes them into its verify block, which
97
+ fails closed unless the file at that exact path carries that nonce.
98
+
99
+ Then, per Step 3 of the partial, use the Write tool to save your composed
100
+ continuation to that exact path. `thrum prime` auto-injects this file on
69
101
  next session start regardless of whether wake comes from `thrum tmux restart` or
70
102
  operator-initiated `thrum tmux create`.
71
103
 
@@ -91,13 +123,44 @@ to preserve — strictly worse than idling. Verify the EFFECT, not the action: a
91
123
  vague "I wrote it" is not verification; check the FILE.
92
124
 
93
125
  ```bash
94
- SNAPSHOT="${REPO}/.thrum/restart/${AGENT}.md"
126
+ SNAPSHOT="<PASTE SNAPSHOT_PATH FROM STEP 3>"
127
+ NONCE="<PASTE SNAPSHOT_NONCE FROM STEP 3>"
128
+ # Fresh authoritative re-resolution (shell state does NOT persist across Bash
129
+ # calls); it must agree with the path you actually wrote in Step 3.
130
+ REPO=$(
131
+ thrum agent worktree --authoritative 2>/dev/null
132
+ ) || { echo "ERROR: cannot authoritatively resolve your worktree via the daemon. Refusing to fall back to cwd/git-toplevel — that would risk silently verifying the wrong repo's snapshot. Check daemon connectivity and retry."; exit 1; }
133
+ [ -n "$REPO" ] || { echo "ERROR: thrum agent worktree --authoritative returned empty"; exit 1; }
134
+ AGENT=$(thrum whoami --field agent_id) || { echo "ERROR: agent not registered"; exit 1; }
135
+ [ -n "$AGENT" ] || { echo "ERROR: empty agent_id"; exit 1; }
136
+ SESSION_RAW=$(thrum whoami --field tmux_session)
137
+ SESSION=${SESSION_RAW%%:*}
138
+ EXPECTED="${REPO}/.thrum/restart/${AGENT}.md"
139
+ BIND_OK=1
140
+ if [ -z "$SNAPSHOT" ] || [ -z "$NONCE" ] || [[ "$SNAPSHOT$NONCE" == *"<PASTE"* ]]; then
141
+ echo "VERIFY FAILED: SNAPSHOT/NONCE not filled in — paste SNAPSHOT_PATH and SNAPSHOT_NONCE from Step 3. SNAPSHOT_OK=0"; BIND_OK=0
142
+ elif [ "$SNAPSHOT" != "$EXPECTED" ]; then
143
+ echo "VERIFY FAILED: identity/worktree DRIFT between write time and verify time. Written path: $SNAPSHOT — re-resolved path: $EXPECTED. SNAPSHOT_OK=0"; BIND_OK=0
144
+ fi
95
145
  NOW=$(date +%s)
96
- MTIME=$(stat -f %m "$SNAPSHOT" 2>/dev/null || stat -c %Y "$SNAPSHOT" 2>/dev/null)
146
+ # mtime: choose the stat dialect explicitly. GNU `stat -f` means --file-system
147
+ # (succeeds with multiline garbage), so the `stat -f %m || stat -c %Y` one-liner
148
+ # breaks on Linux — branch on which dialect this box speaks.
149
+ if stat -f %m . >/dev/null 2>&1; then
150
+ MTIME=$(
151
+ stat -f %m "$SNAPSHOT" 2>/dev/null
152
+ )
153
+ else
154
+ MTIME=$(
155
+ stat -c %Y "$SNAPSHOT" 2>/dev/null
156
+ )
157
+ fi
97
158
  AGE=$(( NOW - ${MTIME:-0} ))
98
159
  SNAPSHOT_OK=1
99
160
 
100
- if [ ! -f "$SNAPSHOT" ]; then
161
+ if [ "$BIND_OK" = 0 ]; then
162
+ SNAPSHOT_OK=0 # binding failure already reported above
163
+ elif [ ! -f "$SNAPSHOT" ]; then
101
164
  echo "VERIFY FAILED: no snapshot at $SNAPSHOT"; SNAPSHOT_OK=0
102
165
  elif [ ! -s "$SNAPSHOT" ]; then
103
166
  echo "VERIFY FAILED: snapshot at $SNAPSHOT is empty"; SNAPSHOT_OK=0
@@ -105,6 +168,8 @@ elif [ ! -r "$SNAPSHOT" ]; then
105
168
  echo "VERIFY FAILED: snapshot at $SNAPSHOT is not readable"; SNAPSHOT_OK=0
106
169
  elif [ "$AGE" -gt 300 ]; then
107
170
  echo "VERIFY FAILED: snapshot at $SNAPSHOT is ${AGE}s old — stale (from a prior restart), not this session's write"; SNAPSHOT_OK=0
171
+ elif ! grep -qF -- "$NONCE" "$SNAPSHOT"; then
172
+ echo "VERIFY FAILED: nonce not found in $SNAPSHOT — file at that path is not this session's write"; SNAPSHOT_OK=0
108
173
  else
109
174
  echo "VERIFY OK: snapshot exists, non-empty, readable, written ${AGE}s ago"
110
175
  fi
@@ -120,8 +185,16 @@ else
120
185
  echo "VERIFY FAILED: tmux session '$SESSION' (stripped from '$SESSION_RAW') does not resolve to a live tmux session"
121
186
  SESSION_OK=0
122
187
  fi
188
+ echo "SNAPSHOT_OK=${SNAPSHOT_OK} SESSION_OK=${SESSION_OK}"
123
189
  ```
124
190
 
191
+ **Fail-closed binding rule:** the block passes only for the exact path and nonce
192
+ the Step 3 PREP block printed (paste them into its first two lines). Empty or
193
+ placeholder values, identity/worktree drift between write and verify time, or a
194
+ file at that path without the nonce all mean VERIFY FAILED — a fresh file merely
195
+ sitting at the re-derived path is NOT proof you wrote it. Use the
196
+ `SNAPSHOT_OK`/`SESSION_OK` values it prints in Step 6.
197
+
125
198
  **"Properly" means, explicitly:** snapshot exists at your OWN worktree's
126
199
  `.thrum/restart/<your-agent-id>.md`, non-empty, readable, AND recently written
127
200
  by Step 3 of THIS session (not a leftover from a previous restart that Step 3
@@ -15,12 +15,17 @@ orchestrate the handoff.
15
15
 
16
16
  ```bash
17
17
  # $REPO must be YOUR worktree — the directory `thrum prime` reads the restart
18
- # snapshot back from. Resolve it from the daemon's authoritative identity, NOT
19
- # `git rev-parse` (which keys off the current shell CWD and would write to the
20
- # wrong .thrum/restart/ if a bash step left your worktree). Fall back to git
21
- # only if whoami can't answer.
22
- REPO=$(thrum whoami --field worktree 2>/dev/null)
23
- [ -n "$REPO" ] || REPO=$(git rev-parse --show-toplevel) || { echo "ERROR: cannot resolve your worktree"; exit 1; }
18
+ # snapshot back from. Resolve it ONLY via the daemon's authoritative,
19
+ # peercred-resolved identity (`thrum agent worktree --authoritative`) — NEVER
20
+ # via `thrum whoami --field worktree` or `git rev-parse --show-toplevel`
21
+ # (both cwd-resolved). A cwd-derived pattern could silently
22
+ # resolve to a DIFFERENT agent's worktree, or the main repo, saving this
23
+ # snapshot where nothing ever reads it back. There is NO git fallback — if
24
+ # the daemon can't answer, refuse and stop rather than guess.
25
+ REPO=$(
26
+ thrum agent worktree --authoritative 2>/dev/null
27
+ ) || { echo "ERROR: cannot authoritatively resolve your worktree via the daemon. Refusing to fall back to cwd/git-toplevel — that would risk silently saving this snapshot to the wrong repo. Check daemon connectivity and retry."; exit 1; }
28
+ [ -n "$REPO" ] || { echo "ERROR: thrum agent worktree --authoritative returned empty"; exit 1; }
24
29
  AGENT=$(thrum whoami --field agent_id) || { echo "ERROR: agent not registered"; exit 1; }
25
30
  [ -n "$AGENT" ] || { echo "ERROR: empty agent_id"; exit 1; }
26
31
  mkdir -p "${REPO}/.thrum/restart"
@@ -130,12 +135,36 @@ always be present so future-you can scan for what's covered and what isn't.
130
135
 
131
136
  ### 3. Write the continuation directly to your restart file
132
137
 
133
- Use your Write tool to save the composed continuation to:
138
+ First run this PREP block (it mints the path and a per-run nonce):
139
+
140
+ ```bash
141
+ # The Bash tool does NOT persist shell state across calls, so this block
142
+ # re-resolves identity itself. Fail closed: no cwd/git fallback.
143
+ REPO=$(
144
+ thrum agent worktree --authoritative 2>/dev/null
145
+ ) || { echo "ERROR: cannot authoritatively resolve your worktree via the daemon. Refusing to fall back to cwd/git-toplevel — that would risk silently saving this snapshot to the wrong repo. Check daemon connectivity and retry."; exit 1; }
146
+ [ -n "$REPO" ] || { echo "ERROR: thrum agent worktree --authoritative returned empty"; exit 1; }
147
+ AGENT=$(thrum whoami --field agent_id) || { echo "ERROR: agent not registered"; exit 1; }
148
+ [ -n "$AGENT" ] || { echo "ERROR: empty agent_id"; exit 1; }
149
+ mkdir -p "${REPO}/.thrum/restart"
150
+ SNAPSHOT="${REPO}/.thrum/restart/${AGENT}.md"
151
+ NONCE="zn6fx-$(date +%s)-$$-${RANDOM}${RANDOM}"
152
+ echo "SNAPSHOT_PATH=${SNAPSHOT}"
153
+ echo "SNAPSHOT_NONCE=${NONCE}"
154
+ ```
155
+
156
+ Write to EXACTLY the printed `SNAPSHOT_PATH` (not a path you re-derive), and make
157
+ sure the file contains the marker below (as its final line), with the printed `SNAPSHOT_NONCE` — verify searches the whole file for it:
134
158
 
135
159
  ```text
136
- ${REPO}/.thrum/restart/${AGENT}.md
160
+ <!-- snapshot-nonce: <SNAPSHOT_NONCE> -->
137
161
  ```
138
162
 
163
+ **Record both values** — the verify step pastes them into its verify block, which
164
+ fails closed unless the file at that exact path carries that nonce.
165
+
166
+ Then use your Write tool to save the composed continuation to that exact path.
167
+
139
168
  `thrum prime` will auto-inject this file at next session start. No bash heredoc
140
169
  or `cat <<EOF` redirection is needed — write the file directly.
141
170
 
@@ -161,13 +190,44 @@ to preserve — strictly worse than idling. Verify the EFFECT, not the action: a
161
190
  vague "I wrote it" is not verification; check the FILE.
162
191
 
163
192
  ```bash
164
- SNAPSHOT="${REPO}/.thrum/restart/${AGENT}.md"
193
+ SNAPSHOT="<PASTE SNAPSHOT_PATH FROM STEP 3>"
194
+ NONCE="<PASTE SNAPSHOT_NONCE FROM STEP 3>"
195
+ # Fresh authoritative re-resolution (shell state does NOT persist across Bash
196
+ # calls); it must agree with the path you actually wrote in Step 3.
197
+ REPO=$(
198
+ thrum agent worktree --authoritative 2>/dev/null
199
+ ) || { echo "ERROR: cannot authoritatively resolve your worktree via the daemon. Refusing to fall back to cwd/git-toplevel — that would risk silently verifying the wrong repo's snapshot. Check daemon connectivity and retry."; exit 1; }
200
+ [ -n "$REPO" ] || { echo "ERROR: thrum agent worktree --authoritative returned empty"; exit 1; }
201
+ AGENT=$(thrum whoami --field agent_id) || { echo "ERROR: agent not registered"; exit 1; }
202
+ [ -n "$AGENT" ] || { echo "ERROR: empty agent_id"; exit 1; }
203
+ SESSION_RAW=$(thrum whoami --field tmux_session)
204
+ SESSION=${SESSION_RAW%%:*}
205
+ EXPECTED="${REPO}/.thrum/restart/${AGENT}.md"
206
+ BIND_OK=1
207
+ if [ -z "$SNAPSHOT" ] || [ -z "$NONCE" ] || [[ "$SNAPSHOT$NONCE" == *"<PASTE"* ]]; then
208
+ echo "VERIFY FAILED: SNAPSHOT/NONCE not filled in — paste SNAPSHOT_PATH and SNAPSHOT_NONCE from Step 3. SNAPSHOT_OK=0"; BIND_OK=0
209
+ elif [ "$SNAPSHOT" != "$EXPECTED" ]; then
210
+ echo "VERIFY FAILED: identity/worktree DRIFT between write time and verify time. Written path: $SNAPSHOT — re-resolved path: $EXPECTED. SNAPSHOT_OK=0"; BIND_OK=0
211
+ fi
165
212
  NOW=$(date +%s)
166
- MTIME=$(stat -f %m "$SNAPSHOT" 2>/dev/null || stat -c %Y "$SNAPSHOT" 2>/dev/null)
213
+ # mtime: choose the stat dialect explicitly. GNU `stat -f` means --file-system
214
+ # (succeeds with multiline garbage), so the `stat -f %m || stat -c %Y` one-liner
215
+ # breaks on Linux — branch on which dialect this box speaks.
216
+ if stat -f %m . >/dev/null 2>&1; then
217
+ MTIME=$(
218
+ stat -f %m "$SNAPSHOT" 2>/dev/null
219
+ )
220
+ else
221
+ MTIME=$(
222
+ stat -c %Y "$SNAPSHOT" 2>/dev/null
223
+ )
224
+ fi
167
225
  AGE=$(( NOW - ${MTIME:-0} ))
168
226
  SNAPSHOT_OK=1
169
227
 
170
- if [ ! -f "$SNAPSHOT" ]; then
228
+ if [ "$BIND_OK" = 0 ]; then
229
+ SNAPSHOT_OK=0 # binding failure already reported above
230
+ elif [ ! -f "$SNAPSHOT" ]; then
171
231
  echo "VERIFY FAILED: no snapshot at $SNAPSHOT"; SNAPSHOT_OK=0
172
232
  elif [ ! -s "$SNAPSHOT" ]; then
173
233
  echo "VERIFY FAILED: snapshot at $SNAPSHOT is empty"; SNAPSHOT_OK=0
@@ -175,6 +235,8 @@ elif [ ! -r "$SNAPSHOT" ]; then
175
235
  echo "VERIFY FAILED: snapshot at $SNAPSHOT is not readable"; SNAPSHOT_OK=0
176
236
  elif [ "$AGE" -gt 300 ]; then
177
237
  echo "VERIFY FAILED: snapshot at $SNAPSHOT is ${AGE}s old — stale (from a prior restart), not this session's write"; SNAPSHOT_OK=0
238
+ elif ! grep -qF -- "$NONCE" "$SNAPSHOT"; then
239
+ echo "VERIFY FAILED: nonce not found in $SNAPSHOT — file at that path is not this session's write"; SNAPSHOT_OK=0
178
240
  else
179
241
  echo "VERIFY OK: snapshot exists, non-empty, readable, written ${AGE}s ago"
180
242
  fi
@@ -190,8 +252,16 @@ else
190
252
  echo "VERIFY FAILED: tmux session '$SESSION' (stripped from '$SESSION_RAW') does not resolve to a live tmux session"
191
253
  SESSION_OK=0
192
254
  fi
255
+ echo "SNAPSHOT_OK=${SNAPSHOT_OK} SESSION_OK=${SESSION_OK}"
193
256
  ```
194
257
 
258
+ **Fail-closed binding rule:** the block passes only for the exact path and nonce
259
+ the Step 3 PREP block printed (paste them into its first two lines). Empty or
260
+ placeholder values, identity/worktree drift between write and verify time, or a
261
+ file at that path without the nonce all mean VERIFY FAILED — a fresh file merely
262
+ sitting at the re-derived path is NOT proof you wrote it. Use the
263
+ `SNAPSHOT_OK`/`SESSION_OK` values it prints in Step 6.
264
+
195
265
  **"Properly" means, explicitly:** snapshot exists at your OWN worktree's
196
266
  `.thrum/restart/<your-agent-id>.md`, non-empty, readable, AND recently written
197
267
  by Step 3 of THIS session (not a leftover from a previous restart that Step 3
@@ -267,9 +337,17 @@ the operator:
267
337
 
268
338
  ## After Restart: Session Archive
269
339
 
270
- After restart, your snapshot doesn't disappear — it moves to
340
+ After restart, IF you saved a snapshot within the graceful window, it moves to
271
341
  `.thrum/agents/<your-agent-id>/sessions/` and stays there as a persistent log
272
- entry. Browse the archive with:
342
+ entry. If the graceful save window elapsed before you saved (e.g. you were
343
+ mid-task and missed the interrupt), the daemon writes NO snapshot and records
344
+ a `.timeout-marker` sibling file instead — `thrum tmux restart` reports this
345
+ via `snapshot_outcome: timeout` (CLI shows a 🔴 warning) rather than silently
346
+ looking like success. Do not infer success from an empty `.thrum/restart/`
347
+ directory alone — an empty source directory is consistent with either a
348
+ genuine successful archive OR a bug that resolved to the wrong destination,
349
+ silently aliased another agent's archive, or wrote truncated content. Browse
350
+ the archive with:
273
351
 
274
352
  ```bash
275
353
  thrum agent sessions list # default: this agent
@@ -278,6 +356,20 @@ thrum agent sessions list --json # NDJSON for scripts
278
356
  thrum agent sessions list --all # every agent, grouped
279
357
  ```
280
358
 
359
+ **Verify the RESOLVED DESTINATION's identity, not just its existence.**
360
+ Every archive already self-verifies: `sessionarchive.Archive` computes a
361
+ SHA-256 checksum of the snapshot content it read *before* the move, then
362
+ re-reads the file it just wrote at the destination and confirms the
363
+ checksums match (`sessionarchive.VerifyArchivedIdentity` /
364
+ `ChecksumContent` — see `internal/daemon/sessionarchive/verify.go`) before
365
+ reporting success. A destination that exists but doesn't hash-match its
366
+ own pre-move content is treated as a hard failure of the archive call, not
367
+ a warning. This is what actually proves "the file at
368
+ `.thrum/agents/<id>/sessions/<ts>-restart.md` IS the artifact that was
369
+ captured," as opposed to merely "a file with a plausible name is sitting
370
+ there" — the latter is what `thrum agent sessions list`'s presence-only
371
+ view alone would let you assume.
372
+
281
373
  Permissions are user-only (`0600` for each snapshot file, `0700` for the
282
374
  sessions folder). Operators on multi-user machines must copy explicitly to share
283
375
  archives with another account.
@@ -11,7 +11,7 @@ content.
11
11
  thrum send --to @agent_name --stdin <<'EOF' # Direct message (quoted heredoc body)
12
12
  message here
13
13
  EOF
14
- thrum send --to @everyone --stdin <<'EOF' # Broadcast to all agents
14
+ thrum send --to @agent_a --to @agent_b --stdin <<'EOF' # Multiple explicit recipients
15
15
  message here
16
16
  EOF
17
17
  ```
@@ -19,6 +19,9 @@ EOF
19
19
  Unknown recipients are a hard error. Use `thrum team` to verify agent names
20
20
  before sending.
21
21
 
22
+ `@everyone` was removed by design (P0 ruling); do not restore it — name
23
+ every recipient explicitly, or address a scoped agent-type group.
24
+
22
25
  If the body has backticks, `$(...)`, `$VAR`, or quotes, pass it via a quoted
23
26
  heredoc so the shell doesn't corrupt it:
24
27