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
@@ -53,8 +53,11 @@ grep "## Review Gate:" <prompt-file-path>
53
53
  # 4. Dependencies configured
54
54
  bd dep tree <epic-id>
55
55
 
56
- # 5. Merge target in config
57
- cat .thrum/config.json | grep merge_target
56
+ # 5. Merge target in config (resolve through .thrum/redirect when present --
57
+ # a worktree's .thrum/ holds only a `redirect` file, not its own config.json)
58
+ CONFIG=.thrum/config.json
59
+ [ -f .thrum/redirect ] && CONFIG="$(cat .thrum/redirect)/config.json"
60
+ cat "$CONFIG" | grep merge_target
58
61
  ```
59
62
 
60
63
  ### Step 2b: Triage CANDIDATE beads (if the dispatch carries any)
@@ -79,8 +82,8 @@ by the result:
79
82
  > `choosing-subagent-models` skill's job, not this one — see it for the tiers. What
80
83
  > matters here is that you pass one at all: the `roleDefault()` backstop in the
81
84
  > daemon covers `thrum tmux launch` of thrum agents, **not** Agent-tool sub-agents,
82
- > which inherit the PARENT model when unspecified. An orchestrator on Opus that
83
- > omits `model:` gets Opus-priced triage.
85
+ > which inherit the PARENT model when unspecified. An orchestrator whose own tier is
86
+ > costly that omits `model:` gets costly-priced triage.
84
87
 
85
88
  | Result | Action |
86
89
  |---|---|
@@ -181,13 +184,13 @@ thrum worktree create <name>
181
184
  # Pin --model on BOTH create AND launch — create's persist to runtime_config.json
182
185
  # is asynchronous and can lose the race to launch's resolution if launch omits
183
186
  # the flag (see CLAUDE.md "Launching an Agent"). There is no create-only shortcut.
184
- # Always launch implementers at --model sonnet. sonnet-medium is the floor;
185
- # no lower tier exists.
187
+ # Always launch implementers pinned to the tier from runtime.role_models, per
188
+ # choosing-subagent-models; that tier is the floor, no lower tier exists.
186
189
  thrum tmux create <name> --cwd <worktree-path> \
187
190
  --name <agent_name> --role implementer --module <module> \
188
191
  --mode ephemeral --identity ephemeral \
189
- --model sonnet
190
- thrum tmux launch <name> --runtime <runtime> --model sonnet
192
+ --model <from runtime.role_models, per choosing-subagent-models>
193
+ thrum tmux launch <name> --runtime <runtime> --model <from runtime.role_models, per choosing-subagent-models>
191
194
  ```
192
195
 
193
196
  > **Implementer lifecycle:** All implementers are ephemeral+stateless (no agent
@@ -215,7 +218,7 @@ it fails again, escalate to the human.
215
218
  ### Step 3b: Verify the model pin took
216
219
 
217
220
  If `--model` is dropped anywhere in the create/launch sequence, the implementer
218
- launches UNPINNED and the runtime defaults to Opus — the exact expensive
221
+ launches UNPINNED and the runtime defaults to its own (costly) model — the exact expensive
219
222
  failure this tier exists to prevent. Confirm the pin landed before assigning
220
223
  work, using `thrum tmux capture --format=annotated` (see CLAUDE.md "Launching
221
224
  an Agent — Verify the Model by Pane, Never by `runtime-config get`" for the
@@ -226,12 +229,12 @@ NBSP fallback trap, and the by-position read):
226
229
  thrum tmux capture <agent-name> --format=annotated --lines 12
227
230
  ```
228
231
 
229
- That prints the runtime's own footer line, e.g. `Model: Sonnet 5`. **Read the
232
+ That prints the runtime's own footer line, e.g. `Model: <resolved model>`. **Read the
230
233
  intended tier from `.thrum/config.json` → `runtime.role_models`, not from memory.**
231
- If the footer shows a different tier (see the BLOCKED note under Step 2 — there is
232
- no valid `haiku` pin), the pin failed silently — re-pin with
233
- `thrum tmux create ... --model sonnet` (or
234
- `thrum agent runtime-config set <agent_name> --model sonnet`) and relaunch BEFORE
234
+ If the footer shows a different tier (see the BLOCKED note under Step 2 — the model ban
235
+ list lives in `choosing-subagent-models`), the pin failed silently — re-pin with
236
+ `thrum tmux create ... --model <from runtime.role_models, per choosing-subagent-models>` (or
237
+ `thrum agent runtime-config set <agent_name> --model <from runtime.role_models, per choosing-subagent-models>`) and relaunch BEFORE
235
238
  assigning work. **Do not dispatch an unpinned implementer** — the daemon's
236
239
  role-default backstop (`jq -r '.runtime.role_models' .thrum/config.json`) is a
237
240
  net for a forgotten pin, not a substitute for this check.
@@ -329,9 +332,9 @@ Handle each message type:
329
332
  2. Dispatch BOTH reviewers IN PARALLEL (one Agent call per reviewer in the same
330
333
  response). Block until BOTH return:
331
334
  - Code-quality: `superpowers:code-reviewer` (or `feature-dev:code-reviewer`
332
- if the project provides one) — `model: "sonnet"`
335
+ if the project provides one) — `model: <per choosing-subagent-models>`
333
336
  - Spec-compliance: `general-purpose` reviewer cross-referencing the plan/
334
- spec — `model: "sonnet"`
337
+ spec — `model: <per choosing-subagent-models>`
335
338
  3. **Verify** every cited `file:line` claim against the actual source before
336
339
  forwarding. Reviewers can misread files; forwarding unverified findings
337
340
  wastes the implementer's time.
@@ -389,10 +392,7 @@ Handle each message type:
389
392
  **Sub-agent model discipline (applies to ALL Agent tool calls in this phase and
390
393
  Phase 5):**
391
394
 
392
- > **Model tiers:** pass an explicit `model:` on every dispatch — `sonnet`
393
- > (low effort) mechanical, `sonnet` (medium effort) judgment, Opus only on
394
- > operator-ask or a skill step that names it. See the
395
- > `choosing-subagent-models` skill for the full policy.
395
+ > **Model tiers:** pass an explicit `model:` on every dispatch; choose model and effort per the `choosing-subagent-models` skill (read `runtime.role_models` at the moment of use). Never let a sub-agent inherit your own model.
396
396
 
397
397
  **Blocker:**
398
398
 
@@ -508,7 +508,7 @@ in Scope Boundaries). Analyze the conflict using your plan/intent context to
508
508
  determine the correct resolution. Then spawn a targeted edit sub-agent:
509
509
 
510
510
  ```text
511
- Agent(subagent_type="general-purpose", model="sonnet",
511
+ Agent(subagent_type="general-purpose", model="<per choosing-subagent-models>",
512
512
  prompt="Resolve this specific merge conflict.
513
513
  File: <file-path>
514
514
  Conflicted section: <paste the <<<< ==== >>>> block>
@@ -618,18 +618,30 @@ can act on, which is how a guard gets disabled. Classify every hit — only a
618
618
  classifier failure is a real only-copy:
619
619
 
620
620
  ```bash
621
- # STAGE 2 — CLASSIFY every hit from stage 1 against ALL HISTORY, not the tip
622
- # tree (a tip-only check like `git cat-file -e origin/<ref>:<path>` flags
623
- # every file the 19-session sliding window has legitimately trimmed as
624
- # unrecoverable — over-reporting, not under-reporting, but still an alarm
625
- # nobody can act on):
626
- git log --all --oneline -- ".thrum/agents/<path>" | head -1
627
- # NON-EMPTY -> RECOVERABLE (in git history)
628
- # EMPTY -> ONLY-COPY — SALVAGE BEFORE REMOVAL
629
-
630
- # CONTROLS — the classifier must be able to both affirm and refuse:
631
- git log --all --oneline -- ".thrum/agents/<a-real-trimmed-path>" # must be NON-EMPTY
632
- git log --all --oneline -- ".thrum/agents/__nope__/x" # must be EMPTY
621
+ # STAGE 2 — classify every stage-1 hit. Test that a BLOB IS RETRIEVABLE,
622
+ # never that the path merely appears in the log: `git log -- <path>` also
623
+ # matches the commit that DELETED the file, and `| head -1` returns exactly
624
+ # that for a trimmed file.
625
+ git log --all --oneline --name-status -- ".thrum/agents/<path>" # read the A/M/D column; do NOT head -1
626
+ git cat-file -e "<add-or-modify-sha>:.thrum/agents/<path>" # rc=0 proves the BLOB EXISTS
627
+ git show "<that-sha>:.thrum/agents/<path>" > /tmp/x && cmp /tmp/x "<worktree-copy>"
628
+ # blob retrievable AND byte-identical -> RECOVERABLE
629
+ # otherwise -> ONLY-COPY — SALVAGE BEFORE REMOVAL
630
+ # Always check rc explicitly on git show/cat-file; never infer from output size.
631
+ # Form the cmp control from two files KNOWN to differ — comparing an
632
+ # accidentally-empty extraction against /dev/null reports "identical" and
633
+ # reads as a passing control.
634
+
635
+ # CONTROLS — the classifier must be able to both affirm and refuse.
636
+ # Exercise BOTH halves of the decision: existence (cat-file -e) AND
637
+ # byte-identity (cmp). Controlling only existence leaves the cmp step
638
+ # unguarded — and cmp is where an accidentally-empty extraction reads
639
+ # as "identical" and passes.
640
+ git cat-file -e "<add-or-modify-sha>:.thrum/agents/<path>"; echo "rc=$? (must be 0)"
641
+ git cat-file -e "<add-or-modify-sha>:.thrum/agents/__nope__/x"; echo "rc=$? (must be NON-ZERO)"
642
+ printf 'x' > /tmp/ctl-differs
643
+ cmp -s /tmp/x /tmp/ctl-differs; echo "rc=$? (must be NON-ZERO — proves cmp CAN detect a difference)"
644
+ cmp -s /tmp/x /tmp/x; echo "rc=$? (must be 0 — proves cmp CAN affirm)"
633
645
  ```
634
646
 
635
647
  ```bash
@@ -719,7 +731,7 @@ Run review rounds; converge to zero BLOCKING:
719
731
  **Round N:**
720
732
 
721
733
  1. Dispatch BOTH reviewers IN PARALLEL — code-quality (`feature-dev:code-reviewer`,
722
- `model: "sonnet"`) + spec-compliance (`general-purpose`, `model: "sonnet"`).
734
+ `model: <per choosing-subagent-models>`) + spec-compliance (`general-purpose`, `model: <per choosing-subagent-models>`).
723
735
  **In each reviewer dispatch prompt, mandate structured severity:**
724
736
  > "Label every finding BLOCKING / IMPORTANT / MINOR. At least one label per
725
737
  > finding, prefix each finding line."
@@ -775,7 +787,7 @@ them, forward to coordinator at merge-approval.
775
787
  **Hard cap — 2 rounds max:**
776
788
  If BLOCKING findings remain after round 2:
777
789
 
778
- - Step A: REMOVED. sonnet-medium is the implementer floor, so there is no
790
+ - Step A: REMOVED. The implementer tier is the floor, so there is no
779
791
  lower tier to escalate from. A round-2 BLOCKING failure goes straight to
780
792
  Step B.
781
793
  - Step B (escalate up): if the implementer fails to converge, escalate to coordinator
@@ -42,25 +42,115 @@ every agent in your `roster`:
42
42
  (your own pane capture) — see "Declared-intent reminders" below for the
43
43
  full mechanics.
44
44
 
45
- ## The modal bright line (non-negotiable)
46
-
47
- - Approve ONLY clearly-safe, recognized, non-`rm`/non-`--force` modals.
48
- - `rm`, `--force`, or an unrecognized command → refuse or escalate to your
49
- `parent`. That filter is mechanical.
50
- - Within the safe set, judge the COMMAND TEXT, not the modal's phrasing: read
51
- the command and approve if it is clearly-safe and recognized whatever wording
52
- the modal uses, else cancel or escalate. Never key on fixed prompt phrases;
53
- never approve blind.
45
+ ## The modal judgment rubric (non-negotiable)
46
+
47
+ - Approve a modal when you can explain what the command does, it fits what
48
+ the requesting agent is visibly doing in its pane, and a mistake would be
49
+ recoverable. Otherwise escalate to your `parent` with a one-line reason.
50
+ - Judge the TARGET, not the keyword: `rm -rf /tmp/<dir the agent just
51
+ created>` is routine cleanup; `rm -rf /tmp/*` is not. One `rm` of a stale
52
+ lock file by a coordinator mid-deploy is fine. A force-push, or a delete
53
+ whose target or purpose you can't tell, is an escalate.
54
+ - Escalating is always allowed. Refusing only because a keyword appeared is
55
+ not.
56
+ - Judge the COMMAND TEXT, not the modal's phrasing: read the command and
57
+ approve or refuse on what it actually does, whatever wording the modal
58
+ uses. Never key on fixed prompt phrases; never approve blind.
54
59
  - Verify by re-capture after acting — never trust exit status alone.
55
- - To unblock a REMOTE roster member's modal, use `thrum tmux send <agent> " "`
56
- (proxies by agent name to the owning peer daemon and appends Enter, so a
57
- space+Enter confirms the default-focused option); for a specific selection
58
- use `thrum tmux send <agent> --keys <K1,K2,...>`. `thrum tmux send` is
59
- peer-routed, so it works identically local or remote (fleet-wide) — this is
60
- the replacement for the retired local-only tmux-key primitive. This grants only
61
- the leftmost/default option; the bright line above still applies. From the
62
- CLI/peer-router path this send is QUEUED behind the conservative
63
- monitor-silence wait, not instant — don't re-send while waiting it out.
60
+
61
+ ### Approving or dismissing a modal: keys only, never text
62
+
63
+ > 🔴 **NEVER answer a modal with a text payload — not even a single
64
+ > character, and never a bare positional argument to `thrum tmux send`.**
65
+ > Plain text TYPES INTO the agent's input box; it does not act as a
66
+ > keypress. If the modal isn't actually showing, or focus differs from what
67
+ > you expect, that text lands in the agent's next command line and can
68
+ > corrupt or execute something you never intended. The only correct
69
+ > primitive is `--keys`, on every call, with no exceptions.
70
+
71
+ The procedure, every time, no shortcuts:
72
+
73
+ 1. **Capture the pane fresh.** Never act on a stale capture from an earlier
74
+ cycle.
75
+ 2. **Recognize the modal and which option is currently focused** (`❯` or
76
+ equivalent marks the default). Judge it against the rubric above.
77
+ 3. **Choose keys, never text:**
78
+ - Default/focused option → `thrum tmux send <agent> --keys Enter`.
79
+ - A specific numbered option → the digit, e.g. `thrum tmux send <agent>
80
+ --keys 1`. Named keys (`Enter`, `Escape`, `Up`, `Down`, `Left`, `Right`,
81
+ `Tab`, digits) deliver in order, comma-separated or repeated
82
+ (`--keys Down,Enter` or `--keys Down --keys Enter`) — confirm the exact
83
+ syntax with `thrum tmux send --help` if in doubt, never guess it.
84
+ - To dismiss without selecting → `thrum tmux send <agent> --keys Escape`.
85
+ - This send is QUEUED behind the conservative monitor-silence wait, not
86
+ instant — don't re-send while waiting it out. A send that exits 3 (Enter
87
+ WITHHELD) or 4 (verdict unknown) must NOT be retried blind — capture the
88
+ pane first.
89
+ 4. **Re-capture and verify the modal is actually gone.** If it's still there,
90
+ don't give up and don't loop blindly retrying the same keys — work up an
91
+ escalating transport ladder, re-capturing to verify after EACH attempt:
92
+ a. `thrum tmux send <agent> --keys ...` — the daemon RPC you already tried
93
+ in step 3. It is peer-routed (works for a local member always, and for
94
+ a remote one when the `can_send_keys` capability is granted between
95
+ your box and theirs) — retry once in case the first send was dropped,
96
+ not blindly beyond that.
97
+ b. **Raw local tmux**, for a member on YOUR OWN box whose pane the daemon
98
+ RPC isn't reaching correctly: `tmux send-keys -t <session> <key>`, then
99
+ `tmux capture-pane -t <session> -p` to verify directly, bypassing the
100
+ daemon layer.
101
+ c. **SSH to the member's own box**, for a remote member or whenever (a)
102
+ fails with a proxy-failure signature (`rpcrouter: caller-peer lacks
103
+ required capability`, `peer unreachable`, `circuit open, dial skipped`)
104
+ — use the shipped pair of scripts, don't hand-roll the SSH call:
105
+ `resources/thrum-watch-pane-capture.sh`'s `build_topology_table` /
106
+ `resolve_roster_targets` resolves the target's SSH destination from
107
+ `thrum state get agent_pool:<agent>` (`.entry.value.box`) joined
108
+ against `thrum state show topology:<scope>` (`.entry.value.hostname`
109
+ / `.entry.value.ssh.target` / `.entry.value.ssh.user` /
110
+ `.entry.value.repo_path`), then hands those already-resolved values to
111
+ `resources/thrum-capture-fallback.sh <capture|key> <agent> <ssh_target>
112
+ <ssh_user> <repo_path> [keys...]`, which is the piece that actually
113
+ runs `ssh "<ssh_user>@<ssh_target>" "cd '<repo_path>' && thrum tmux
114
+ send '<agent>' --keys <keys>"` — `thrum-capture-fallback.sh` does no
115
+ resolution of its own, so calling it directly requires you (or the
116
+ caller) to already have those three values. A topology gap fails
117
+ closed (no SSH fallback attempted) rather than guessing a wrong
118
+ destination. There is no per-agent SSH field in the roster/agent Go
119
+ schema — the destination comes from this `thrum state` topology data.
120
+ (There IS a real `.thrum/ssh/config` on fleet boxes — the underlying
121
+ host-alias file, included via `~/.ssh/config`, that makes a bare `ssh
122
+ <box>` resolve at all — but it is not what these scripts read; they
123
+ read the resolved `ssh.target`/`ssh.user` values out of `thrum state`
124
+ instead.)
125
+ Only escalate to your `parent` after exhausting this ladder, or
126
+ immediately on an UNRECOGNIZED modal — and when you do, include the fresh
127
+ capture.
128
+ - Cross-peer send-keys being capability-gated is a deliberate security
129
+ boundary (§ladder step a above), not a bug to route around by other means
130
+ without checking the failure signature first. A peer can always CAPTURE
131
+ another peer's panes read-only regardless of capability.
132
+
133
+ **Worked example** — the standard two-variant Claude Code tool-confirmation
134
+ modal (`internal/daemon/permission/patterns.go`, pattern `tool_confirmation`,
135
+ dialect `claude-tui`), captured verbatim:
136
+
137
+ ```
138
+ ⏺ Bash(curl https://example.com)
139
+ ⎿ Do you want to proceed?
140
+ 1. Yes
141
+ 2. Yes, and don't ask again for Bash(curl)
142
+ 3. No, and tell Claude what to do differently (Esc)
143
+ ```
144
+
145
+ - **Approve once** → `--keys 1`.
146
+ - **Deny, with feedback** → `--keys 3` (or `--keys Escape` on the two-option
147
+ Read-prompt variant, which has no "No, and tell Claude" line).
148
+ - **NEVER `--keys 2`** — "Yes, and don't ask again" grants a standing
149
+ allow-rule beyond this one command; the modal judgment rubric above is a
150
+ per-command judgment, not a license to mint blanket permissions. This is
151
+ pinned by `ApproveKey: "1", // Yes (once) — NEVER "2"` in
152
+ `internal/daemon/permission/patterns.go` and tested by
153
+ `TestApproveKeyNeverForeverAllow`.
64
154
 
65
155
  ## Your own restart (auto-restart-at-ctx-threshold)
66
156
 
@@ -121,6 +211,22 @@ is the entire reason the watch exists — and a blocked coordinator cannot
121
211
  receive its own escalation. The escalation would arrive at the one inbox
122
212
  guaranteed not to be read.
123
213
 
214
+ ### 🔴 HUMAN-NEEDED escalation goes to the owner by message, never pane-only
215
+
216
+ When you need a human decision, resolve the owner from CONFIG — never hardcode
217
+ a name — and deliver it as a message, not pane text:
218
+
219
+ OWNER=$(jq -r '.permission_supervisors[] | select(startswith("@user:"))' .thrum/config.json | sed 's/^@//' | head -1)
220
+ thrum send --to "$OWNER" --stdin <<'EOF'
221
+ [ESCALATION] <what needs a decision>: <why>. Action needed: <what>.
222
+ EOF
223
+
224
+ Writing the escalation only into your own pane is not delivery — nobody reads
225
+ watcher panes. If you get no response within 15 minutes, resend and verify
226
+ delivery (`thrum sent --unread`) before assuming it was seen. SAFE recognized
227
+ modals stay approver-of-record and are approved directly, unchanged — this
228
+ rule applies only to escalations that need a human decision.
229
+
124
230
  ### An unwatchable roster member: escalate every cycle until it is removed
125
231
 
126
232
  A roster member you **cannot watch at all** — capture fails by every path
@@ -217,8 +323,8 @@ Register the loop as a `thrum monitor` instead — a daemon-scheduled job,
217
323
  independent of your session, that keeps running across your restarts.
218
324
 
219
325
  **A ready-to-use script ships with this skill** — `resources/thrum-watch-
220
- pane-capture.sh` (paired with `resources/thrum-capture-fallback.sh`,
221
- thrum-3mhrt). It is generic and roster-driven: it reads your own
326
+ pane-capture.sh` (paired with `resources/thrum-capture-fallback.sh`).
327
+ It is generic and roster-driven: it reads your own
222
328
  `watch_params.json`, captures each roster member's pane (local `thrum tmux
223
329
  capture` first, always — that already reaches remote agents by name via the
224
330
  normal rpcrouter proxy; see "SSH fallback" below for what the second script
@@ -246,10 +352,10 @@ runtime-versioned path involved:
246
352
 
247
353
  Then register the monitor against that absolute copy path —
248
354
  **`--notify-on-success` is MANDATORY, not optional decoration**: a
249
- `--schedule`d job delivers NOTHING on `--match` alone (thrum-ruz1z §5c —
250
- this fails completely silently, every status field reads healthy, and it
355
+ `--schedule`d job delivers NOTHING on `--match` alone — this fails
356
+ completely silently, every status field reads healthy, and it
251
357
  is exactly the trap a literal reading of the syntax below used to walk
252
- readers into):
358
+ readers into:
253
359
 
254
360
  ```bash
255
361
  thrum monitor start --name <you>-pane-watch \
@@ -267,13 +373,54 @@ At the start of every cycle, confirm the monitor is still running
267
373
  (`thrum monitor list`) — if it's missing or dead, re-`thrum monitor start`
268
374
  it rather than assuming someone else will notice.
269
375
 
376
+ **Roster snapshot monitor:** copy `resources/roster-watch.sh`,
377
+ `resources/capture_archive.py`, and `resources/jev_watcher_filter.py` into `<worktree>/.thrum-watch/`, then
378
+ `chmod +x <worktree>/.thrum-watch/roster-watch.sh` — a mode-preserving copy of
379
+ this resource can land as `644` depending on the runtime plugin's own tree
380
+ (don't rely on the source file's mode alone) — then register the monitor
381
+ against that copied `roster-watch.sh`. Keep the helper beside the script; the
382
+ roster archive path must not depend on a plugin or research worktree path at
383
+ run time. Captures and their archive land under `.thrum/agents/<you>/watch-captures/`
384
+ (redirect-resolved), not beside the deployed script itself.
385
+
386
+ Register roster-watch.sh's own monitor the same way, matching its actual
387
+ output lines (`"roster capture ready:"` for a normal tick, `"ROSTER
388
+ INCIDENT:"` for a capture-failure incident):
389
+
390
+ ```bash
391
+ thrum monitor start --name roster-watch-<you> \
392
+ --match "^(roster capture ready:|ROSTER INCIDENT:)" \
393
+ --to @<you> \
394
+ --env AGENT_NAME=<you> \
395
+ --notify-on-success \
396
+ --schedule '*/10 * * * *' \
397
+ -- <worktree>/.thrum-watch/roster-watch.sh
398
+ ```
399
+
400
+ Set `use_jev: true` in the watcher's redirect-resolved `watch_params.json`
401
+ to filter notifications. JEV is opt-in ONLY when a key is actually present:
402
+ `use_jev: true` alone does not engage it. The helper reads API keys from the
403
+ watcher worktree's repo-root `.env` (or `ROSTER_JEV_ENV_FILE` if set);
404
+ environment variables take precedence. `OPENROUTER_API_KEY` is an optional
405
+ provider fallback. If `use_jev: true` but neither `THRUM_TYPESAFE_KEY` nor
406
+ `OPENROUTER_API_KEY` is found (env or that `.env` file), the watcher logs
407
+ `JEV requested but no key found — skipped` and runs the plain non-JEV path
408
+ for that tick — no manifest filter, no decision directory, and
409
+ `jev_watcher_filter.py` never runs. Confident active permission prompts and
410
+ blocking user-choice dialogs wake the watcher. Failed or uncertain decisions use
411
+ the normal watcher path. Missing or false `use_jev` retains normal behavior.
412
+
413
+ Keep --notify-on-success when changing use_jev. A compatible daemon
414
+ suppresses the success confirmation only after a completed, confidently ordinary
415
+ JEV tick; changing the boolean needs no monitor re-registration.
416
+
270
417
  ### SSH fallback (resilience net, not the primary path)
271
418
 
272
419
  `thrum tmux capture <agent>` already reaches remote agents by name via the
273
420
  rpcrouter proxy — that is how fleet-wide watching works today for a healthy
274
421
  peer. `thrum-capture-fallback.sh` exists for when that proxy path is
275
- broken on the caller's side (known bug classes: thrum-7vwgy same-host
276
- dual-daemon caller-identity collision; thrum-zkqut phantom-routing
422
+ broken on the caller's side (known bug classes: a same-host
423
+ dual-daemon caller-identity collision; a phantom-routing
277
424
  regression on a daemon build predating `EnsureProxies`). It is a strict
278
425
  FALLBACK, never primary and never SSH-first: `thrum-watch-pane-capture.sh`
279
426
  always tries the local proxy path first, and only retries via an SSH hop —