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,291 @@
1
+ #!/usr/bin/env bash
2
+ # roster-watch_test.sh — unit tests for roster-watch.sh's
3
+ # resolve_watch_params() (the ".thrum/redirect" fix), plus a
4
+ # coverage bonus for thrum-watch-pane-capture.sh's PARAMS_FILE resolution
5
+ # logic (inline harness — that script isn't structured as sourceable
6
+ # functions the way roster-watch.sh is; see Scenario 3 below).
7
+ #
8
+ # Run: bash claude-plugin/skills/persistent-watcher-archetype/resources/roster-watch_test.sh
9
+ # (or: SCRIPT_UNDER_TEST=/path/to/alt-roster-watch.sh bash roster-watch_test.sh
10
+ # to run Scenario 1 against a different implementation, e.g. for a manual
11
+ # RED-before/GREEN-after check against a deliberately-reverted copy.)
12
+ #
13
+ # All tests are hermetic: every fixture is a throwaway tree under mktemp -d,
14
+ # cleaned up via a trap; nothing here touches the real repo's .thrum, a live
15
+ # daemon, or the network.
16
+ set -uo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ SCRIPT_UNDER_TEST="${SCRIPT_UNDER_TEST:-$SCRIPT_DIR/roster-watch.sh}"
20
+
21
+ # shellcheck source=/dev/null
22
+ source "$SCRIPT_UNDER_TEST" || {
23
+ echo "roster-watch.sh not found/sourceable at $SCRIPT_UNDER_TEST -- resolve_watch_params is missing" >&2
24
+ exit 1
25
+ }
26
+
27
+ pass=0
28
+ fail=0
29
+ assert_eq() {
30
+ local desc="$1" expected="$2" actual="$3"
31
+ if [ "$expected" = "$actual" ]; then echo "PASS: $desc"; pass=$((pass + 1));
32
+ else echo "FAIL: $desc -- expected '$expected' got '$actual'"; fail=$((fail + 1)); fi
33
+ }
34
+ assert_ne() {
35
+ local desc="$1" unexpected="$2" actual="$3"
36
+ if [ "$unexpected" != "$actual" ]; then echo "PASS: $desc"; pass=$((pass + 1));
37
+ else echo "FAIL: $desc -- '$actual' unexpectedly equals '$unexpected'"; fail=$((fail + 1)); fi
38
+ }
39
+
40
+ # roster_count <file> -- number of entries in the "roster" array of a
41
+ # watch_params.json fixture. Uses python3 (already a hard dependency of
42
+ # roster-watch.sh itself, e.g. load_roster()).
43
+ roster_count() {
44
+ python3 -c '
45
+ import json, sys
46
+ try:
47
+ d = json.load(open(sys.argv[1]))
48
+ print(len(d.get("roster", [])))
49
+ except Exception:
50
+ print(-1)
51
+ ' "$1" 2>/dev/null
52
+ }
53
+
54
+ # mk_watch_params <file> <n> -- write a watch_params.json fixture with a
55
+ # roster array of n plain-string entries.
56
+ mk_watch_params() {
57
+ local file="$1" n="$2"
58
+ mkdir -p "$(dirname "$file")"
59
+ python3 -c '
60
+ import json, sys
61
+ n = int(sys.argv[1])
62
+ json.dump({"roster": [f"agent{i}" for i in range(n)]}, open(sys.argv[2], "w"))
63
+ ' "$n" "$file"
64
+ }
65
+
66
+ AGENT="watcher_primary"
67
+
68
+ # =============================================================================
69
+ # Scenario 1 (MANDATORY, cmain-required regression guard): a worktree that
70
+ # redirects to a main-repo tree. The "roster-editing agent's Edit-tool write"
71
+ # lands in the REDIRECT TARGET (main-repo) copy -- that's the physically
72
+ # correct, shared, redirect-resolved location per internal/paths/paths.go
73
+ # AgentDir ("the agents/ tree is shared, not per-worktree"), and it's what
74
+ # resolve_watch_params must read. A worktree-LOCAL copy at the naive,
75
+ # unresolved path is a decoy: it's what a broken script (SCRIPT_DIR-relative,
76
+ # no redirect-follow) would read instead, and it must be ignored.
77
+ #
78
+ # Bead framing: main-repo (correct) copy has 25 entries ("not 25" in the
79
+ # bug's own words -- i.e. the FIXED behavior must see 25); the worktree-local
80
+ # decoy has 24 (the stale/wrong count an unfixed script would see instead).
81
+ # =============================================================================
82
+ echo "--- Scenario 1: regression guard (redirect-resolved read wins over worktree-local decoy) ---"
83
+
84
+ worktree_root="$(mktemp -d)"
85
+ mainrepo_root="$(mktemp -d)"
86
+ trap 'rm -r -- "$worktree_root" "$mainrepo_root" 2>/dev/null || true' EXIT
87
+
88
+ mkdir -p "$worktree_root/.thrum"
89
+ printf '%s/.thrum' "$mainrepo_root" > "$worktree_root/.thrum/redirect"
90
+
91
+ mainrepo_params="$mainrepo_root/.thrum/agents/$AGENT/watch_params.json"
92
+ worktree_decoy_params="$worktree_root/.thrum/agents/$AGENT/watch_params.json"
93
+
94
+ mk_watch_params "$mainrepo_params" 25 # the real edit, correctly-wired fixture
95
+ mk_watch_params "$worktree_decoy_params" 24 # the stale worktree-local decoy
96
+
97
+ resolved="$(resolve_watch_params "$worktree_root" "$AGENT")"
98
+ assert_eq "resolves to the redirect-target (main-repo) path" "$mainrepo_params" "$resolved"
99
+ assert_ne "does NOT resolve to the worktree-local decoy path" "$worktree_decoy_params" "$resolved"
100
+ assert_eq "resolved file has the EDITED count (25, not 24)" "25" "$(roster_count "$resolved")"
101
+
102
+ # --- Negative control: prove the resolver is really reading the redirect
103
+ # target's CONTENT, not something incidental (e.g. a hardcoded/dead-end
104
+ # path, or a test bug where both fixtures happen to satisfy the assertion
105
+ # regardless of which tree holds what). Swap which tree holds the "right"
106
+ # (25) vs "wrong" (24) count, WITHOUT changing which path resolve_watch_params
107
+ # points at. If the resolver is doing real redirect-following, the resolved
108
+ # PATH stays the main-repo path (structural, content-independent), but the
109
+ # roster COUNT read from it must now be 24 (the swapped-in wrong count) --
110
+ # i.e. the same "expect 25" assertion used above must now correctly FAIL,
111
+ # proving the earlier PASS wasn't vacuous.
112
+ echo "--- Scenario 1 (mis-wired control): swapped fixture must make the same assertion fail ---"
113
+ mk_watch_params "$mainrepo_params" 24 # now the main-repo copy is "stale"
114
+ mk_watch_params "$worktree_decoy_params" 25 # decoy now happens to hold the "right" count
115
+
116
+ resolved_mis="$(resolve_watch_params "$worktree_root" "$AGENT")"
117
+ assert_eq "mis-wired: resolved path is unchanged (still the main-repo path)" "$mainrepo_params" "$resolved_mis"
118
+ got_count="$(roster_count "$resolved_mis")"
119
+ if [ "$got_count" = "25" ]; then
120
+ echo "FAIL: mis-wired control -- expected count MISMATCH (resolver should read the swapped-in 24, not the decoy's 25) but got 25; this would mean the test passes regardless of fixture wiring"
121
+ fail=$((fail + 1))
122
+ else
123
+ echo "PASS: mis-wired control -- resolver reads 24 (the main-repo tree's current content), confirming it is not incidentally reading the decoy"
124
+ pass=$((pass + 1))
125
+ fi
126
+
127
+ # restore correct wiring for anything downstream that might reuse these dirs
128
+ mk_watch_params "$mainrepo_params" 25
129
+ mk_watch_params "$worktree_decoy_params" 24
130
+
131
+ rm -r -- "$worktree_root" "$mainrepo_root"
132
+ trap - EXIT
133
+
134
+ # =============================================================================
135
+ # Scenario 2: existing (non-worktree) behavior unchanged -- no .thrum/redirect
136
+ # file at all means this IS the main repo; resolve_watch_params must fall
137
+ # through to the LOCAL .thrum/agents/<agent>/watch_params.json.
138
+ # =============================================================================
139
+ echo "--- Scenario 2: no redirect file -> resolves to local .thrum path (no regression) ---"
140
+
141
+ mainrepo_only="$(mktemp -d)"
142
+ trap 'rm -r -- "$mainrepo_only" 2>/dev/null || true' EXIT
143
+
144
+ local_params="$mainrepo_only/.thrum/agents/$AGENT/watch_params.json"
145
+ mk_watch_params "$local_params" 7
146
+ # deliberately no redirect file created
147
+
148
+ resolved_local="$(resolve_watch_params "$mainrepo_only" "$AGENT")"
149
+ assert_eq "no-redirect case resolves to the local path" "$local_params" "$resolved_local"
150
+ assert_eq "no-redirect case reads the local file's real content" "7" "$(roster_count "$resolved_local")"
151
+
152
+ rm -r -- "$mainrepo_only"
153
+ trap - EXIT
154
+
155
+ # =============================================================================
156
+ # Scenario 3 (bonus): thrum-watch-pane-capture.sh's PARAMS_FILE resolution.
157
+ # That script is NOT structured as sourceable functions (its redirect logic
158
+ # is a flat sequence of top-level assignments between REPO_ROOT and
159
+ # FALLBACK_SCRIPT, guarded by no function boundary) -- sourcing the whole
160
+ # file would run its lock-dir/roster-load/capture logic as a side effect.
161
+ # So this reproduces JUST that resolution snippet as a tiny inline harness,
162
+ # verified byte-for-byte against the live file below (so the harness can't
163
+ # silently drift from the real script) before asserting on it.
164
+ # =============================================================================
165
+ echo "--- Scenario 3 (bonus): thrum-watch-pane-capture.sh PARAMS_FILE resolution (inline harness) ---"
166
+
167
+ pane_capture_script="$SCRIPT_DIR/thrum-watch-pane-capture.sh"
168
+ if [ ! -f "$pane_capture_script" ]; then
169
+ echo "SKIP: $pane_capture_script not found -- skipping Scenario 3"
170
+ else
171
+ expected_snippet='THRUM_DIR="${REPO_ROOT}/.thrum"
172
+ if [ -f "${THRUM_DIR}/redirect" ]; then
173
+ THRUM_DIR="$(tr -d '"'"'[:space:]'"'"' < "${THRUM_DIR}/redirect")"
174
+ fi
175
+ PARAMS_FILE="${THRUM_DIR}/agents/${AGENT_NAME}/watch_params.json"'
176
+ live_snippet="$(sed -n '/^THRUM_DIR="\${REPO_ROOT}\/\.thrum"$/,/^PARAMS_FILE=/p' "$pane_capture_script")"
177
+ if [ "$live_snippet" != "$expected_snippet" ]; then
178
+ echo "FAIL: Scenario 3 harness has drifted from the live PARAMS_FILE resolution snippet in $pane_capture_script -- update the harness (this is a test-maintenance guard, not a product assertion)"
179
+ echo " live snippet was:"
180
+ echo "$live_snippet" | sed 's/^/ /'
181
+ fail=$((fail + 1))
182
+ else
183
+ echo "PASS: inline harness matches the live PARAMS_FILE resolution snippet verbatim"
184
+ pass=$((pass + 1))
185
+
186
+ resolve_pane_capture_params() {
187
+ # Args: repo_root, agent_name. Mirrors the verbatim snippet above.
188
+ local REPO_ROOT="$1" AGENT_NAME="$2" THRUM_DIR
189
+ THRUM_DIR="${REPO_ROOT}/.thrum"
190
+ if [ -f "${THRUM_DIR}/redirect" ]; then
191
+ THRUM_DIR="$(tr -d '[:space:]' < "${THRUM_DIR}/redirect")"
192
+ fi
193
+ printf '%s/agents/%s/watch_params.json\n' "${THRUM_DIR}" "${AGENT_NAME}"
194
+ }
195
+
196
+ pc_worktree="$(mktemp -d)"
197
+ pc_mainrepo="$(mktemp -d)"
198
+ trap 'rm -r -- "$pc_worktree" "$pc_mainrepo" 2>/dev/null || true' EXIT
199
+
200
+ mkdir -p "$pc_worktree/.thrum"
201
+ printf '%s/.thrum' "$pc_mainrepo" > "$pc_worktree/.thrum/redirect"
202
+ pc_mainrepo_params="$pc_mainrepo/.thrum/agents/$AGENT/watch_params.json"
203
+ pc_decoy_params="$pc_worktree/.thrum/agents/$AGENT/watch_params.json"
204
+ mk_watch_params "$pc_mainrepo_params" 25
205
+ mk_watch_params "$pc_decoy_params" 24
206
+
207
+ pc_resolved="$(resolve_pane_capture_params "$pc_worktree" "$AGENT")"
208
+ assert_eq "pane-capture: redirect present -> resolves to main-repo path" "$pc_mainrepo_params" "$pc_resolved"
209
+ assert_ne "pane-capture: does NOT resolve to worktree-local decoy" "$pc_decoy_params" "$pc_resolved"
210
+ assert_eq "pane-capture: resolved file has the edited count (25)" "25" "$(roster_count "$pc_resolved")"
211
+
212
+ rm -r -- "$pc_worktree" "$pc_mainrepo"
213
+ trap - EXIT
214
+
215
+ pc_mainrepo_only="$(mktemp -d)"
216
+ trap 'rm -r -- "$pc_mainrepo_only" 2>/dev/null || true' EXIT
217
+ pc_local_params="$pc_mainrepo_only/.thrum/agents/$AGENT/watch_params.json"
218
+ mk_watch_params "$pc_local_params" 3
219
+ pc_resolved_local="$(resolve_pane_capture_params "$pc_mainrepo_only" "$AGENT")"
220
+ assert_eq "pane-capture: no redirect -> resolves to local path" "$pc_local_params" "$pc_resolved_local"
221
+ rm -r -- "$pc_mainrepo_only"
222
+ trap - EXIT
223
+ fi
224
+ fi
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # Scenario 4: load_roster's capture_daemon_overrides field.
228
+ # Regression guard for a REAL bug caught during that scrub: the plain-string
229
+ # roster-entry branch of load_roster's python emitter originally printed
230
+ # only 3 `|` separators (agent|||override) for what is now a 5-FIELD
231
+ # schema (agent|host|tmux|runtime|override, needing 4 separators) -- the
232
+ # missing separator silently shifted every subsequent field left by one,
233
+ # so a string-roster-entry agent's override always read back empty even
234
+ # when set. Covers both roster-entry shapes (plain string and the
235
+ # {agent_id,hostname,tmux_session} dict form) so a similar off-by-one in
236
+ # either emitter branch fails loud here instead of silently dropping an
237
+ # agent's --daemon-id routing in production.
238
+ # ---------------------------------------------------------------------------
239
+ echo "--- Scenario 4: capture_daemon_overrides read into ROSTER_DAEMON_OVERRIDE ---"
240
+ d4_dir="$(mktemp -d)"
241
+ trap 'rm -r -- "$d4_dir" 2>/dev/null || true' EXIT
242
+ d4_params="$d4_dir/watch_params.json"
243
+ python3 -c '
244
+ import json, sys
245
+ data = {
246
+ "roster": [
247
+ "agent_plain_no_override",
248
+ "agent_plain_with_override",
249
+ {"agent_id": "agent_dict_with_override", "hostname": "boxc", "tmux_session": "sess"},
250
+ ],
251
+ "capture_daemon_overrides": {
252
+ "agent_plain_with_override": "d_PLAINOVERRIDE00000000000",
253
+ "agent_dict_with_override": "d_DICTOVERRIDE0000000000000",
254
+ },
255
+ }
256
+ json.dump(data, open(sys.argv[1], "w"))
257
+ ' "$d4_params"
258
+
259
+ load_roster "$d4_params"
260
+
261
+ assert_eq "capture_daemon_overrides: roster has 3 entries" "3" "${#ROSTER[@]}"
262
+ assert_eq "capture_daemon_overrides: entry 0 agent id" "agent_plain_no_override" "${ROSTER[0]}"
263
+ assert_eq "capture_daemon_overrides: plain string entry with NO override reads empty" "" "${ROSTER_DAEMON_OVERRIDE[0]}"
264
+ assert_eq "capture_daemon_overrides: entry 1 agent id" "agent_plain_with_override" "${ROSTER[1]}"
265
+ assert_eq "capture_daemon_overrides: plain string entry WITH override reads its daemon id" "d_PLAINOVERRIDE00000000000" "${ROSTER_DAEMON_OVERRIDE[1]}"
266
+ assert_eq "capture_daemon_overrides: entry 2 agent id" "agent_dict_with_override" "${ROSTER[2]}"
267
+ assert_eq "capture_daemon_overrides: dict-form entry's hostname still reads correctly (field alignment intact)" "boxc" "${ROSTER_HOSTNAME[2]}"
268
+ assert_eq "capture_daemon_overrides: dict-form entry's tmux_session still reads correctly" "sess" "${ROSTER_TMUX_SESSION[2]}"
269
+ assert_eq "capture_daemon_overrides: dict-form entry WITH override reads its daemon id" "d_DICTOVERRIDE0000000000000" "${ROSTER_DAEMON_OVERRIDE[2]}"
270
+
271
+ rm -r -- "$d4_dir"
272
+ trap - EXIT
273
+
274
+ # Default/absent capture_daemon_overrides -> every entry inert (empty).
275
+ d4b_dir="$(mktemp -d)"
276
+ trap 'rm -r -- "$d4b_dir" 2>/dev/null || true' EXIT
277
+ d4b_params="$d4b_dir/watch_params.json"
278
+ python3 -c '
279
+ import json, sys
280
+ json.dump({"roster": ["agent_a", "agent_b"]}, open(sys.argv[1], "w"))
281
+ ' "$d4b_params"
282
+ load_roster "$d4b_params"
283
+ assert_eq "capture_daemon_overrides absent from watch_params.json: entry 0 inert" "" "${ROSTER_DAEMON_OVERRIDE[0]}"
284
+ assert_eq "capture_daemon_overrides absent from watch_params.json: entry 1 inert" "" "${ROSTER_DAEMON_OVERRIDE[1]}"
285
+ rm -r -- "$d4b_dir"
286
+ trap - EXIT
287
+
288
+ # ---------------------------------------------------------------------------
289
+ echo "----------------------------------------"
290
+ echo "roster-watch: $pass passed, $fail failed"
291
+ [ "$fail" -eq 0 ]
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # thrum-capture-fallback.sh — capture/key an agent, falling back to an SSH
3
3
  # hop when the local `thrum tmux capture|key` proxy path fails with a known
4
- # cross-daemon proxy-routing error signature (thrum-7vwgy / thrum-zkqut
5
- # class bugs: same-host dual-daemon caller-identity collision, and a
4
+ # cross-daemon proxy-routing error signature (known bug classes:
5
+ # same-host dual-daemon caller-identity collision, and a
6
6
  # phantom-routing regression on daemon builds predating EnsureProxies).
7
7
  #
8
8
  # `thrum tmux capture <agent>` ALREADY reaches remote agents by name via the
@@ -3,7 +3,7 @@
3
3
  # thrum-watch-pane-capture.sh
4
4
  #
5
5
  # The generic, roster-driven pane-watch capture-loop script that ships with
6
- # the persistent-watcher-archetype plugin skill (thrum-3mhrt). Works for
6
+ # the persistent-watcher-archetype plugin skill. Works for
7
7
  # BOTH a persistent-watcher-archetype watcher and the brainstorm-steward
8
8
  # (one parameterized script serves both — the only differences, roster
9
9
  # contents and cadence, are already `watch_params.json` fields, not script
@@ -18,8 +18,8 @@
18
18
  # capture each member's pane (local `thrum tmux capture` first, ALWAYS —
19
19
  # `thrum tmux capture <agent>` already reaches remote agents by name via
20
20
  # the normal rpcrouter proxy; the SSH hop below is a resilience FALLBACK
21
- # for when that path is broken on the caller's side, e.g. thrum-7vwgy/
22
- # thrum-zkqut-class bugs, never a fleet-wide-watching enabler in its own
21
+ # for when that path is broken on the caller's side, e.g. known
22
+ # cross-daemon proxy-routing bug classes, never a fleet-wide-watching enabler in its own
23
23
  # right, and never SSH-first) -> run each capture through `thrum detect
24
24
  # --category permission` -> for a STABLE (unchanged across two captures,
25
25
  # two seconds apart) detected permission prompt, flag it in the report ->
@@ -55,7 +55,7 @@
55
55
  #
56
56
  # THRUM MONITOR EXAMPLE (see SKILL.md for the full, current recipe —
57
57
  # --notify-on-success is MANDATORY: a --schedule'd job delivers NOTHING on
58
- # --match alone, thrum-ruz1z/S5c):
58
+ # --match alone):
59
59
  # thrum monitor start --name <you>-pane-watch \
60
60
  # --match "^watch-tick: (cycle done|RESOLUTION-FAIL)" \
61
61
  # --to @<you> \
@@ -72,7 +72,19 @@ AGENT_NAME="$(basename -- "${SCRIPT_DIR}")"
72
72
  # -- never hardcode REPO_ROOT (the prior box-specific adaptation this
73
73
  # script generalizes did, and that was exactly the thing to fix).
74
74
  REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." >/dev/null 2>&1 && pwd)"
75
- PARAMS_FILE="${SCRIPT_DIR}/watch_params.json"
75
+ # Resolve watch_params.json through .thrum/redirect. The
76
+ # agents/ tree is the SHARED, redirect-resolved (main-repo) tree, not
77
+ # per-worktree (see internal/paths/paths.go AgentDir) -- reading it
78
+ # straight off SCRIPT_DIR (this copy's own worktree-local location) reads
79
+ # the wrong, stale file whenever this script is deployed into a feature
80
+ # worktree that redirects: a roster-editing agent's Edit-tool write lands
81
+ # in the redirect-resolved main-repo copy, and this script would silently
82
+ # keep reading the worktree-local one and never see the edit.
83
+ THRUM_DIR="${REPO_ROOT}/.thrum"
84
+ if [ -f "${THRUM_DIR}/redirect" ]; then
85
+ THRUM_DIR="$(tr -d '[:space:]' < "${THRUM_DIR}/redirect")"
86
+ fi
87
+ PARAMS_FILE="${THRUM_DIR}/agents/${AGENT_NAME}/watch_params.json"
76
88
  FALLBACK_SCRIPT="${SCRIPT_DIR}/thrum-capture-fallback.sh"
77
89
 
78
90
  LOG_DIR="${REPO_ROOT}/.thrum/var/log"
@@ -139,7 +151,7 @@ fi
139
151
  # script (no mapfile/readarray, no `local -n`, no bash-4 associative
140
152
  # arrays -- parallel indexed arrays instead). Test with `/bin/bash
141
153
  # thrum-watch-pane-capture.sh`, not just `bash ...` or `./...`, or this
142
- # class of bug only surfaces on the live scheduled run (thrum-dnatz).
154
+ # class of bug only surfaces on the live scheduled run.
143
155
  ROSTER=()
144
156
  while IFS= read -r line; do
145
157
  [ -n "${line}" ] && ROSTER+=("${line}")
@@ -182,7 +194,7 @@ log "roster: ${ROSTER[*]}"
182
194
  # against the current CLI, regardless of how the field/scope names read.
183
195
  is_placeholder() {
184
196
  case "$1" in
185
- ""|PENDING-LEON|PENDING|TODO|TBD) return 0 ;;
197
+ ""|PENDING|TODO|TBD) return 0 ;;
186
198
  *) return 1 ;;
187
199
  esac
188
200
  }
@@ -267,7 +279,7 @@ resolve_roster_targets() {
267
279
  build_topology_table
268
280
  resolve_roster_targets
269
281
 
270
- # Fork default (coordinator-resolved, thrum-3mhrt plan): if EVERY roster
282
+ # Fork default (coordinator-resolved plan): if EVERY roster
271
283
  # agent failed resolution, that is itself worth surfacing as a real
272
284
  # problem (likely a fleet-wide topology/agent_pool data gap), not a clean
273
285
  # tick -- flip the summary line's marker so `--match` treats it as a
@@ -81,6 +81,16 @@ lifetime.
81
81
  If the session will exceed ~30 minutes, or coordination with other agents is
82
82
  needed, defer to `thrum prime`.
83
83
 
84
+ **Exception: post-`/thrum:compact` resume — MANUAL FALLBACK ONLY.** After
85
+ `/thrum:compact` or `/thrum:compact-extended`, the SessionStart hook now
86
+ auto-injects a zero-turn light briefing (`thrum prime --light`) directly into
87
+ context on a healthy daemon — no turn spent reading a file or running a skill.
88
+ This skill is the correct choice here only as a **fallback**, when that
89
+ auto-injection didn't fire or failed (e.g. the hook's "auto-injection failed"
90
+ notice, daemon unreachable). In that fallback case: read the snapshot first,
91
+ then run this skill. See `claude-plugin/commands/compact.md`'s "How resume
92
+ works" section.
93
+
84
94
  ## After Step 2
85
95
 
86
96
  Once both steps complete, proceed with the work the wake-primer asked for.
@@ -381,16 +381,13 @@ bd dep add <later-epic-id> <earlier-epic-id>
381
381
  ### Create Tasks
382
382
 
383
383
  When creating > 6 tasks, delegate to parallel sub-agents — one per epic. Each
384
- sub-agent (sonnet-low is sufficient — the work is mechanical) gets the epic ID, the
384
+ sub-agent (a cheap tier is sufficient — the work is mechanical; model per `choosing-subagent-models`) gets the epic ID, the
385
385
  list of `bd create --title=... --type=task --priority=N --description=...`
386
386
  commands to run, the within-epic `bd dep add <later_id> <earlier_id>` ordering
387
387
  commands, and instructions to return the created task IDs and titles. Invoke
388
388
  `efficient-multi-agent-research` § Core Pattern for launch-and-wait mechanics.
389
389
 
390
- > **Model tiers:** pass an explicit `model:` on every dispatch — `sonnet`
391
- > (low effort) mechanical, `sonnet` (medium effort) judgment, Opus only on
392
- > operator-ask or a skill step that names it. See the
393
- > `choosing-subagent-models` skill for the full policy.
390
+ > **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.
394
391
 
395
392
  After sub-agents return IDs, set cross-epic dependencies directly (requires IDs
396
393
  from multiple sub-agents):
@@ -478,20 +475,24 @@ template's "Logging Refactoring Opportunities" section.
478
475
 
479
476
  If not found, create one:
480
477
 
481
- `--description` is multi-line prose — never double-quoted inline. On
482
- `scripts/bd-shared`, `--stdin`/`--body-file` are refused (remote-path
483
- resolution + silent-empty-body hazards), so write it to a scratch file and
484
- pass `-d "$(cat <file>)"`; see the role preamble's 🔴 PROSE INTO A COMMAND
485
- rule.
478
+ `--description` is multi-line prose — never double-quoted inline. Use
479
+ `--body-file` to pass the content from a file instead; see the role preamble's
480
+ 🔴 PROSE INTO A COMMAND rule. `scripts/bd-shared` delivers `--body-file` (and
481
+ `--stdin`) content: it reads the file on your box and pipes those bytes to bd,
482
+ and an empty or blank body is refused loudly; only `-f`/`--file` and `--graph`
483
+ (bulk-plan files) are refused there. Scratch files go in `/private/tmp` on macOS,
484
+ where `/tmp` is a symlink (check `[ -L /tmp ]`), and in `/tmp` elsewhere.
486
485
 
487
486
  ```bash
488
- cat > /tmp/refactor-epic-desc.md <<'EOF'
487
+ # Scratch dir: /private/tmp on macOS (where /tmp is a symlink), /tmp elsewhere.
488
+ SCRATCH=$([ -L /tmp ] && echo /private/tmp || echo /tmp)
489
+ cat > "$SCRATCH/refactor-epic-desc.md" <<'EOF'
489
490
  Persistent backlog for refactoring, DRY improvements, and code
490
491
  organization opportunities discovered during feature work. Tasks are added by
491
492
  implementation agents as they encounter opportunities. Reviewed and prioritized
492
493
  by the coordinator periodically.
493
494
  EOF
494
- bd create --title="Refactoring & DRY Opportunities" --type=epic --priority=3 -d "$(cat /tmp/refactor-epic-desc.md)"
495
+ bd create --title="Refactoring & DRY Opportunities" --type=epic --priority=3 --body-file "$SCRATCH/refactor-epic-desc.md"
495
496
  ```
496
497
 
497
498
  This epic is **project-wide and long-lived** — it persists across feature epics.
@@ -604,8 +605,9 @@ Include in each option's description:
604
605
 
605
606
  - Path: `~/.workspaces/{repo}/{feature}` (from CLAUDE.md convention)
606
607
  - Branch: `feature/{feature-name}`, cut from the configured merge target
607
- (`jq -r '.orchestration.merge_target' .thrum/config.json`) — never a
608
- remembered branch name
608
+ (`CONFIG=.thrum/config.json; [ -f .thrum/redirect ] && CONFIG="$(cat .thrum/redirect)/config.json"; jq -r '.orchestration.merge_target' "$CONFIG"`
609
+ — a worktree's `.thrum/` holds only a `redirect` file, not its own
610
+ `config.json`) — never a remembered branch name
609
611
 
610
612
  **For agent names**, suggest a name derived from the feature in each option
611
613
  description (e.g., `impl_{feature}`). The user can override.
@@ -626,8 +628,11 @@ git status
626
628
  # (rebase onto a moved base is banned project-wide; see CLAUDE.md
627
629
  # "Rebase onto current tip before sending MERGE-READY")
628
630
  # Resolve the branch, never hardcode it — a name in a document goes stale and
629
- # this one already did:
630
- TARGET=$(jq -r '.orchestration.merge_target' .thrum/config.json)
631
+ # this one already did. Resolve through .thrum/redirect when present -- a
632
+ # worktree's .thrum/ holds only a `redirect` file, not its own config.json:
633
+ CONFIG=.thrum/config.json
634
+ [ -f .thrum/redirect ] && CONFIG="$(cat .thrum/redirect)/config.json"
635
+ TARGET=$(jq -r '.orchestration.merge_target' "$CONFIG")
631
636
  git fetch origin "$TARGET"
632
637
  git merge "origin/$TARGET"
633
638
  # OR if branches diverged: ask user before merging
@@ -810,6 +815,7 @@ worktree-related values come from the Phase 3 assignments:
810
815
  | `{{EPIC_TITLE}}` | Epic title (used in commit messages) |
811
816
  | `{{WORKTREE_PATH}}` | **From Phase 3 worktree assignment** |
812
817
  | `{{BRANCH_NAME}}` | **From Phase 3 worktree assignment** |
818
+ | `{{BASE_BRANCH}}` | Resolved from the project merge target: `CONFIG=.thrum/config.json; [ -f .thrum/redirect ] && CONFIG="$(cat .thrum/redirect)/config.json"; jq -r '.orchestration.merge_target' "$CONFIG"` (a worktree's `.thrum/` holds only a `redirect` file). Never assume `main` |
813
819
  | `{{PROJECT_ROOT}}` | Absolute path to the project root |
814
820
  | `{{DESIGN_DOC}}` | **Absolute path** to the design spec |
815
821
  | `{{REFERENCE_CODE}}` | Relevant reference code paths (relative OK if committed) |