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.
- package/assets/commands/thrum-_compact-runtime-commands.md +61 -0
- package/assets/commands/thrum-_snapshot-protocol.md +17 -6
- package/assets/commands/thrum-_stamp-protocol.md +9 -2
- package/assets/commands/thrum-compact-extended.md +352 -0
- package/assets/commands/thrum-compact.md +300 -0
- package/assets/commands/thrum-inbox.md +1 -1
- package/assets/commands/thrum-restart-extended.md +84 -11
- package/assets/commands/thrum-restart.md +105 -13
- package/assets/commands/thrum-send.md +4 -1
- package/assets/commands/thrum-sleep-extended.md +67 -46
- package/assets/commands/thrum-sleep.md +67 -58
- package/assets/skills/adversarial-critique/SKILL.md +5 -8
- package/assets/skills/brainstorm-steward/SKILL.md +65 -11
- package/assets/skills/brainstormer-staying-current/SKILL.md +1 -4
- package/assets/skills/choosing-subagent-models/SKILL.md +14 -0
- package/assets/skills/conversation-only-context/SKILL.md +11 -11
- package/assets/skills/conversation-only-context/resources/driver-subagent-pattern.md +1 -1
- package/assets/skills/conversation-only-context/resources/remote-environment-scenarios.md +1 -1
- package/assets/skills/coordinator-assessing-agent-completion/SKILL.md +98 -6
- package/assets/skills/coordinator-context-monitoring/SKILL.md +29 -19
- package/assets/skills/coordinator-deploying-a-node/SKILL.md +124 -0
- package/assets/skills/coordinator-dispatching-work/SKILL.md +24 -11
- package/assets/skills/coordinator-hotpath-merge-gate/SKILL.md +62 -20
- package/assets/skills/coordinator-managing-state-and-lifecycle/SKILL.md +6 -6
- package/assets/skills/coordinator-merging-code/SKILL.md +25 -11
- package/assets/skills/coordinator-philosophy-merge-gate/SKILL.md +28 -7
- package/assets/skills/coordinator-plan-reconcile-gate/SKILL.md +3 -3
- package/assets/skills/coordinator-rolling-out-to-nodes/SKILL.md +92 -0
- package/assets/skills/coordinator-running-autonomous-feature-development/SKILL.md +3 -3
- package/assets/skills/coordinator-running-brainstorm-cycles/SKILL.md +21 -20
- package/assets/skills/coordinator-running-review-cycles/SKILL.md +15 -4
- package/assets/skills/diagnosing-fleet-and-daemon/SKILL.md +0 -2
- package/assets/skills/efficient-multi-agent-research/SKILL.md +3 -3
- package/assets/skills/implementer-orchestrating-execution/SKILL.md +11 -14
- package/assets/skills/orchestrate/SKILL.md +47 -35
- package/assets/skills/persistent-watcher-archetype/SKILL.md +172 -25
- package/assets/skills/persistent-watcher-archetype/resources/capture_archive.py +618 -0
- package/assets/skills/persistent-watcher-archetype/resources/jev_watcher_filter.py +837 -0
- package/assets/skills/persistent-watcher-archetype/resources/roster-watch.sh +1140 -0
- package/assets/skills/persistent-watcher-archetype/resources/roster-watch_test.sh +291 -0
- package/assets/skills/persistent-watcher-archetype/resources/thrum-capture-fallback.sh +2 -2
- package/assets/skills/persistent-watcher-archetype/resources/thrum-watch-pane-capture.sh +20 -8
- package/assets/skills/prime-agent/SKILL.md +10 -0
- package/assets/skills/project-setup/SKILL.md +22 -16
- package/assets/skills/project-setup/resources/implementation-agent.md +56 -35
- package/assets/skills/researcher-investigating/SKILL.md +2 -5
- package/assets/skills/thrum/SKILL.md +14 -7
- package/assets/skills/thrum/resources/ANTI_PATTERNS.md +16 -18
- package/assets/skills/thrum/resources/CLI_REFERENCE.md +17 -11
- package/assets/skills/thrum/resources/LISTENER_PATTERN.md +9 -3
- package/assets/skills/thrum/resources/MESSAGING.md +18 -11
- package/assets/skills/thrum/resources/WORKTREES.md +1 -1
- package/assets/skills/thrum-memory/SKILL.md +1 -1
- package/assets/skills/trace-spec-to-plan/resources/sweep-agent-prompt.md +1 -1
- package/assets/skills/update-queue/SKILL.md +6 -8
- package/assets/skills/using-the-dashboard/SKILL.md +30 -0
- package/assets/skills/using-the-queue/SKILL.md +76 -5
- package/assets/skills/using-thrum-state/SKILL.md +83 -0
- package/assets/skills/verify-against-plan/SKILL.md +120 -5
- package/assets/skills/watcher-archetype/SKILL.md +18 -0
- package/assets/skills/writing-durable-markdown/SKILL.md +5 -0
- package/dist/installer.js +4 -4
- package/dist/permission-fixture.integration.test.js +4 -4
- package/package.json +1 -1
- package/assets/skills/coordinator-deploying-the-fleet/SKILL.md +0 -396
- package/dist/allowlist-sync.test.d.ts +0 -1
- 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 (
|
|
5
|
-
#
|
|
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
|
|
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.
|
|
22
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|
-
(`
|
|
608
|
-
|
|
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
|
-
|
|
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) |
|