aicodeman 1.18.1 → 1.18.3
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/README.md +2 -1
- package/dist/hooks-config.d.ts +27 -0
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +44 -0
- package/dist/hooks-config.js.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.445f1584.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/constants.822a486d.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/i18n.86757605.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +1 -1
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.0454a3e9.js.gz +0 -0
- package/dist/web/public/mobile-handlers.bc45f976.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.1eef31fe.css.gz +0 -0
- package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/panels-ui.1e4cb4b2.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/session-ui.20be58fa.js.gz +0 -0
- package/dist/web/public/settings-ui.eacb4f92.js.gz +0 -0
- package/dist/web/public/{styles.1b76e7b3.css → styles.70bf9c2a.css} +1 -1
- package/dist/web/public/styles.70bf9c2a.css.br +0 -0
- package/dist/web/public/{styles.1b76e7b3.css.gz → styles.70bf9c2a.css.gz} +0 -0
- package/dist/web/public/subagent-windows.2a192c3e.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/terminal-ui.c182a68d.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.6cec0fba.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.4ec670b0.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +18 -1
- package/dist/web/routes/session-routes.js.map +1 -1
- package/package.json +1 -1
- package/skills/codeman/SKILL.md +282 -724
- package/skills/codeman/preamble.sh +158 -0
- package/skills/codeman/reference/endpoints.md +1 -1
- package/skills/codeman/reference/recipes.md +24 -11
- package/skills/codeman/reference/verbs.md +665 -0
- package/dist/web/public/styles.1b76e7b3.css.br +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# ---- Codeman agent preamble 1.18.3 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
|
2
|
+
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
|
3
|
+
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
|
4
|
+
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
|
5
|
+
# CODEMAN_PASSWORD already (§6 explains why, and what to do when it has not);
|
|
6
|
+
# the data dir's .env is the documented fallback, the same one `codeman attach`
|
|
7
|
+
# reads. The data dir is wherever the hook-secret file lives. Values may be
|
|
8
|
+
# quoted or `export`-prefixed.
|
|
9
|
+
ENV_FILE="${CODEMAN_HOOK_SECRET_FILE:+${CODEMAN_HOOK_SECRET_FILE%hook-secret}.env}"
|
|
10
|
+
envval() { sed -n "s/^\(export \)\{0,1\}$1=//p" "$ENV_FILE" | tail -1 | sed 's/^"\(.*\)"$/\1/; s/^'\''\(.*\)'\''$/\1/'; }
|
|
11
|
+
if [ -z "${CODEMAN_PASSWORD:-}" ] && [ -n "$ENV_FILE" ] && [ -f "$ENV_FILE" ]; then
|
|
12
|
+
CODEMAN_USERNAME=$(envval CODEMAN_USERNAME)
|
|
13
|
+
CODEMAN_PASSWORD=$(envval CODEMAN_PASSWORD)
|
|
14
|
+
fi
|
|
15
|
+
AUTH=(); [ -n "${CODEMAN_PASSWORD:-}" ] && AUTH=(-u "${CODEMAN_USERNAME:-admin}:$CODEMAN_PASSWORD")
|
|
16
|
+
# -k: harmless on http, required on https (self-signed cert).
|
|
17
|
+
# X-Codeman-Parent-Session: tags workers YOU spawn as your children, so the web UI can
|
|
18
|
+
# draw the lineage. Set once here and every present and future create call carries it;
|
|
19
|
+
# it is ignored on every other endpoint. Purely cosmetic (see §5.1) and it can never
|
|
20
|
+
# fail a spawn, so there is no case where you would want to leave it off.
|
|
21
|
+
CURL=(curl -sk "${AUTH[@]}" -H "X-Codeman-Parent-Session: $SELF")
|
|
22
|
+
CID=codeman-agent-1 # FIXED literal, never "agent-$$": see below
|
|
23
|
+
|
|
24
|
+
# Fail-CLOSED session delete. The DELETE lives INSIDE the guard on purpose: the older
|
|
25
|
+
# `is_self "$SID" || curl -X DELETE ...` shape failed OPEN, because an undefined
|
|
26
|
+
# is_self exits 127 and the `||` branch then ran the delete completely unguarded.
|
|
27
|
+
# Undefined delete_session is "command not found", which deletes nothing.
|
|
28
|
+
delete_session() {
|
|
29
|
+
local id="${1:-}"
|
|
30
|
+
[ -n "$id" ] || { echo "refusing: empty session id"; return 1; }
|
|
31
|
+
[ "${#SELF}" -ge 8 ] || { echo "refusing: \$SELF unset or too short to prove this is not me"; return 1; }
|
|
32
|
+
# ids appear in full AND 8-char form (Docker exports a truncated $SELF; mux names and
|
|
33
|
+
# UI surfaces carry 8-char ids), so compare by prefix in BOTH directions. Equality or
|
|
34
|
+
# a one-directional check each miss a real combination, and the miss deletes you.
|
|
35
|
+
case "$id" in "$SELF"*) echo "refusing: $id is me"; return 1 ;; esac
|
|
36
|
+
case "$SELF" in "$id"*) echo "refusing: $id is me"; return 1 ;; esac
|
|
37
|
+
"${CURL[@]}" -X DELETE "$API/api/v1/sessions/$id"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# ---- fast path: the four verbs, already written. §1 composes them. ----
|
|
41
|
+
_composer_up() { # <sid> <timeoutMs> -> "true"/"false". `shift+tab` is the one token
|
|
42
|
+
"${CURL[@]}" -G "$API/api/v1/sessions/$1/wait-output" \
|
|
43
|
+
--data-urlencode 'match=shift+tab' --data-urlencode 'from=buffer' \
|
|
44
|
+
--data-urlencode "timeout=$2" | jq -r '.data.wait.matched // false'
|
|
45
|
+
}
|
|
46
|
+
# spawn_worker <caseName> [mode] -> session id on stdout, diagnostics on stderr.
|
|
47
|
+
# quick-start AND readiness in one call, with a strict contract: NON-EMPTY stdout means
|
|
48
|
+
# a READY claude worker in a hook-carrying case. Anything less is rc 1 with EMPTY
|
|
49
|
+
# stdout, and the half-spawned session is deleted here rather than handed back, because
|
|
50
|
+
# a worker that never drew its composer would eat the task prompt with its trust
|
|
51
|
+
# dialog. There is deliberately no pid poll: wait-output already blocks until the
|
|
52
|
+
# composer draws, and pid!=null proved startup, never readiness.
|
|
53
|
+
spawn_worker() {
|
|
54
|
+
local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
|
|
55
|
+
# parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
|
|
56
|
+
# curl (or a body someone rebuilt from this recipe) still carries its lineage.
|
|
57
|
+
q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
|
|
58
|
+
-d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" '{caseName:$n,mode:$m,parentSessionId:$p}')")
|
|
59
|
+
sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
|
|
60
|
+
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
|
|
61
|
+
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
|
|
62
|
+
[ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # only claude draws a composer
|
|
63
|
+
# quick-start RESOLVES the name before creating: a linked case or an existing dir
|
|
64
|
+
# wins over a fresh scratch case, so "created => hooks" is only true after this one
|
|
65
|
+
# local grep (the same marker the server itself checks for). No marker means sendwait
|
|
66
|
+
# would false-resolve on flapping idle, possibly inside the user's REAL repo: refuse
|
|
67
|
+
# rather than run the job there.
|
|
68
|
+
cp=$(jq -r '.data.casePath // empty' <<<"$q")
|
|
69
|
+
grep -qs '/api/hook-event' "$cp/.claude/settings.local.json" || {
|
|
70
|
+
echo "case '$name' resolved to '$cp', which has no Codeman hooks (linked or pre-existing?): pick an unused name, or work §5.1+§5.5 by hand" >&2
|
|
71
|
+
delete_session "$sid" >/dev/null; return 1; }
|
|
72
|
+
# Short composer wait FIRST, then the trust-dialog probe: a case still showing the
|
|
73
|
+
# dialog can never pass the composer wait, so probing early keeps a cold case from
|
|
74
|
+
# paying the whole long wait before the fallback even runs (§5.2). A warm case
|
|
75
|
+
# matches in under a second and never reaches the probe.
|
|
76
|
+
r=$(_composer_up "$sid" 5000)
|
|
77
|
+
if [ "$r" != true ]; then
|
|
78
|
+
if "${CURL[@]}" -G "$API/api/v1/sessions/$sid/wait-output" \
|
|
79
|
+
--data-urlencode 'match=trust' --data-urlencode 'from=buffer' --data-urlencode 'timeout=2000' \
|
|
80
|
+
| jq -e '.data.wait.matched' >/dev/null; then
|
|
81
|
+
# Codeman's own auto-accept gives up after 90 s / 3 tries; this is that bounded fallback.
|
|
82
|
+
"${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" -H 'Content-Type: application/json' \
|
|
83
|
+
-d "$(jq -nc --arg c "$CID-$sid" '{input:"\r",useMux:true,clientId:$c,seq:1}')" >/dev/null
|
|
84
|
+
fi
|
|
85
|
+
r=$(_composer_up "$sid" 45000)
|
|
86
|
+
fi
|
|
87
|
+
[ "$r" = true ] || { echo "worker $sid never drew a composer; deleted it. Retry by hand via the §5.2 ladder (its billed stage-4 probe included)" >&2
|
|
88
|
+
delete_session "$sid" >/dev/null; return 1; }
|
|
89
|
+
printf '%s\n' "$sid"
|
|
90
|
+
}
|
|
91
|
+
# spawn_workers <caseName>... -> one "<caseName> <sessionId>" line per worker, in order;
|
|
92
|
+
# the sessionId column is EMPTY for a spawn that failed (stderr has why). CONCURRENT:
|
|
93
|
+
# N workers cost about what one costs. Spawning them one Bash call at a time is the
|
|
94
|
+
# single biggest avoidable delay in this skill. Names must be UNIQUE: two workers in
|
|
95
|
+
# one case directory co-edit the same tree (§4), so a repeat is an error here, not a race.
|
|
96
|
+
spawn_workers() {
|
|
97
|
+
local d n i=0
|
|
98
|
+
[ "$#" -gt 0 ] || { echo "spawn_workers: no case names given" >&2; return 1; }
|
|
99
|
+
[ -z "$(printf '%s\n' "$@" | sort | uniq -d)" ] || { echo "spawn_workers: duplicate case names" >&2; return 1; }
|
|
100
|
+
d=$(mktemp -d "${TMPDIR:-/tmp}/codeman-spawn.XXXXXX") || return 1
|
|
101
|
+
for n in "$@"; do ( spawn_worker "$n" > "$d/$i" ) & i=$((i+1)); done
|
|
102
|
+
wait
|
|
103
|
+
i=0; for n in "$@"; do printf '%s %s\n' "$n" "$(cat "$d/$i" 2>/dev/null)"; i=$((i+1)); done
|
|
104
|
+
rm -rf "$d"
|
|
105
|
+
}
|
|
106
|
+
# sendwait <sid> <prompt> [seq] -> blocks until that worker's turn ENDS (~10 min ceiling
|
|
107
|
+
# across its two waits). One billed turn. The \r and the per-worker clientId are applied
|
|
108
|
+
# here, which is why you never hand-build this body. seq defaults to the CURRENT EPOCH
|
|
109
|
+
# SECOND so that every new prompt is a new frame: the server drops any (clientId,seq)
|
|
110
|
+
# pair it has already applied, so a fixed default would make every later prompt to that
|
|
111
|
+
# worker a silent no-op that still "succeeds" and reports the previous turn's state.
|
|
112
|
+
# Pass seq explicitly for exactly one reason: resending a possibly-delivered frame as a
|
|
113
|
+
# deliberate duplicate, at the SAME number (§5.3).
|
|
114
|
+
# Delivery is SELF-HEALING: an Ink repaint occasionally eats the Enter, leaving the
|
|
115
|
+
# typed prompt stranded on the composer while a long wait runs its whole timeout
|
|
116
|
+
# (observed live). So the first wait is short; on its timeout a bare \r goes out (the
|
|
117
|
+
# missing Enter when the prompt is stranded, a no-op when the turn is genuinely
|
|
118
|
+
# running), then the ORIGINAL frame is resent unchanged, which the server takes as a
|
|
119
|
+
# tagged duplicate: it re-waits without retyping (§5.3). Trustworthy only for a claude
|
|
120
|
+
# worker spawn_worker handed back (hooks vetted); hook-less workspaces and other modes
|
|
121
|
+
# resolve on flapping idle: markers instead (§5.5).
|
|
122
|
+
sendwait() {
|
|
123
|
+
local sid="${1:?}" p="${2:?}" seq="${3:-$(date +%s)}" body r
|
|
124
|
+
body=$(jq -nc --arg p "$p" --arg c "$CID-$sid" --argjson s "$seq" \
|
|
125
|
+
'{input:($p+"\r"),useMux:true,clientId:$c,seq:$s,wait:true,waitTimeout:20000}')
|
|
126
|
+
r=$("${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" \
|
|
127
|
+
-H 'Content-Type: application/json' --data-binary "$body")
|
|
128
|
+
if jq -e '.data.delivered and .data.wait.timedOut' <<<"$r" >/dev/null 2>&1; then
|
|
129
|
+
"${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" -H 'Content-Type: application/json' \
|
|
130
|
+
-d "$(jq -nc --arg c "$CID-$sid" --argjson s "$(date +%s)" \
|
|
131
|
+
'{input:"\r",useMux:true,clientId:$c,seq:$s}')" >/dev/null
|
|
132
|
+
r=$("${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" \
|
|
133
|
+
-H 'Content-Type: application/json' --data-binary "$(jq -c '.waitTimeout=580000' <<<"$body")")
|
|
134
|
+
fi
|
|
135
|
+
printf '%s\n' "$r"
|
|
136
|
+
}
|
|
137
|
+
# last_text <sid> [prev] -> that worker's last assistant message. Polled, because the
|
|
138
|
+
# transcript write LAGS the stop signal, and "some text exists" is not "THIS turn's
|
|
139
|
+
# text exists": right after a SECOND turn on the same worker the endpoint still serves
|
|
140
|
+
# the previous answer for a beat (observed live). When reading consecutive turns, pass
|
|
141
|
+
# the previous answer as [prev]: the poll then holds out for text that differs from it,
|
|
142
|
+
# falling back to whatever it last saw if the budget runs dry, so an honestly repeated
|
|
143
|
+
# answer still comes back. Non-zero exit means the worker really never wrote one.
|
|
144
|
+
last_text() {
|
|
145
|
+
local t="" prev="${2:-}"
|
|
146
|
+
for _ in $(seq 1 15); do
|
|
147
|
+
t=$("${CURL[@]}" "$API/api/v1/sessions/$1/last-response" | jq -r '.data.text // empty')
|
|
148
|
+
[ -n "$t" ] && [ "$t" != "$prev" ] && { printf '%s\n' "$t"; return 0; }
|
|
149
|
+
sleep 1
|
|
150
|
+
done
|
|
151
|
+
[ -n "$t" ] && { printf '%s\n' "$t"; return 0; }
|
|
152
|
+
return 1
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
|
156
|
+
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
|
157
|
+
# here would fail that match and rewrite this file on every single bootstrap.
|
|
158
|
+
CODEMAN_PREAMBLE=1.18.3
|
|
@@ -666,7 +666,7 @@ whose turn already ended just times out, with or without `fresh`, verified live)
|
|
|
666
666
|
Register the waiter before the event can happen: send-and-wait does exactly that,
|
|
667
667
|
and `wait-output` markers with `from=buffer` are latched by construction. Never
|
|
668
668
|
fire-and-forget N prompts and then gather signal-waits worker by worker; every
|
|
669
|
-
worker that finishes before its gather is unobservable (see recipes.md Flow
|
|
669
|
+
worker that finishes before its gather is unobservable (see recipes.md Flow 4).
|
|
670
670
|
|
|
671
671
|
#### `GET /api/v1/sessions/:id/wait`
|
|
672
672
|
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
# Worked orchestration flows
|
|
2
2
|
|
|
3
3
|
Loaded on demand from the `codeman` skill. Every flow assumes the SKILL.md preamble is
|
|
4
|
-
in scope (`$API`, `$SELF`, `$CID`, `"${CURL[@]}"`, `delete_session
|
|
4
|
+
in scope (`$API`, `$SELF`, `$CID`, `"${CURL[@]}"`, `delete_session`, plus the fast-path
|
|
5
|
+
verbs `spawn_worker` / `spawn_workers` / `sendwait` / `last_text`); see
|
|
5
6
|
[SKILL.md §0](../SKILL.md#0-guard-and-bootstrap) for it and
|
|
6
7
|
[the safety rules](../SKILL.md#4-safety-rules) for what you may call unprompted.
|
|
7
8
|
|
|
9
|
+
⚠️ **These flows are the long way round, and most jobs do not need them.** If the job is
|
|
10
|
+
"spawn N claude workers, task them, collect the answers", [SKILL.md
|
|
11
|
+
§1](../SKILL.md#1-the-fast-path-n-workers-one-bash-call) already is that job in one Bash
|
|
12
|
+
call, measured at about 10 s for two cold workers end to end. Come here when you need a
|
|
13
|
+
mechanism §1 does not cover: shell or otherwise hook-less workers (Flows 2, 3), a worker
|
|
14
|
+
stuck on a permission dialog (Flow 5), messaging (Flow 6), or real work in git worktrees
|
|
15
|
+
(Flow 7). The flows below spell each step out because they are teaching the mechanism;
|
|
16
|
+
spelling them out again when §1 would have done is the most common way an agent turns a
|
|
17
|
+
ten-second run into a multi-minute one.
|
|
18
|
+
|
|
8
19
|
⚠️ **Shell state does not survive between tool calls**, so every Bash call below opens
|
|
9
20
|
by sourcing the preamble file the §0 bootstrap wrote, and checking its version stamp:
|
|
10
21
|
|
|
11
22
|
```bash
|
|
12
23
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
|
13
|
-
[ "${CODEMAN_PREAMBLE:-}" = 1.
|
|
24
|
+
[ "${CODEMAN_PREAMBLE:-}" = 1.18.3 ] || { echo "preamble missing or stale; re-run the §0 bootstrap"; exit 1; }
|
|
14
25
|
```
|
|
15
26
|
|
|
16
27
|
Do **not** re-paste the preamble body into each call. Sourcing it is what retires the
|
|
@@ -272,16 +283,18 @@ live (and one anti-pattern, measured failing, replaced by B):
|
|
|
272
283
|
**A. Background the send-and-waits** (simplest; each resolved on `stop` while the
|
|
273
284
|
other was still running). Each send costs its worker one billed turn:
|
|
274
285
|
|
|
286
|
+
`sendwait <sid> <prompt> [seq]` is a preamble function ([SKILL.md
|
|
287
|
+
§0](../SKILL.md#0-guard-and-bootstrap)); it applies the `\r` and a per-worker `clientId`,
|
|
288
|
+
and picks a fresh `seq` (the current epoch second) per call, so do not redefine it here
|
|
289
|
+
and pass `seq` yourself only to resend an identical frame as a deliberate duplicate.
|
|
290
|
+
Background one call per worker and `wait`:
|
|
291
|
+
|
|
275
292
|
```bash
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
( sendwait "$SID1" 'refactor module A and reply DONE' 2 & \
|
|
283
|
-
sendwait "$SID2" 'write tests for module B and reply DONE' 2 & wait )
|
|
284
|
-
jq -c '.data.wait | {signal, waitedMs}' /tmp/fan-"$SID1".json /tmp/fan-"$SID2".json
|
|
293
|
+
D=$(mktemp -d) # a function's stdout is per-worker, so collect it in files, not a var
|
|
294
|
+
sendwait "$SID1" 'refactor module A and reply DONE' > "$D/1" &
|
|
295
|
+
sendwait "$SID2" 'write tests for module B and reply DONE' > "$D/2" &
|
|
296
|
+
wait
|
|
297
|
+
jq -c '.data.wait | {signal, waitedMs}' "$D/1" "$D/2"; rm -rf "$D"
|
|
285
298
|
```
|
|
286
299
|
|
|
287
300
|
One in-flight wait per worker keeps you far from the 16-per-session waiter cap.
|