aicodeman 1.14.0 → 1.14.2

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 (93) hide show
  1. package/README.md +12 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +225 -28
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config/webview-limits.d.ts +17 -1
  6. package/dist/config/webview-limits.d.ts.map +1 -1
  7. package/dist/config/webview-limits.js +18 -2
  8. package/dist/config/webview-limits.js.map +1 -1
  9. package/dist/hooks-config.d.ts +44 -0
  10. package/dist/hooks-config.d.ts.map +1 -1
  11. package/dist/hooks-config.js +153 -2
  12. package/dist/hooks-config.js.map +1 -1
  13. package/dist/web/ports/config-port.d.ts +2 -0
  14. package/dist/web/ports/config-port.d.ts.map +1 -1
  15. package/dist/web/public/admin-ui.js.gz +0 -0
  16. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  17. package/dist/web/public/{app.b24f795e.js → app.94825d85.js} +9 -9
  18. package/dist/web/public/app.94825d85.js.br +0 -0
  19. package/dist/web/public/app.94825d85.js.gz +0 -0
  20. package/dist/web/public/constants.e7175b95.js.gz +0 -0
  21. package/dist/web/public/cron-ui.js.gz +0 -0
  22. package/dist/web/public/entrance-animations.js.gz +0 -0
  23. package/dist/web/public/i18n.da34cfaf.js.gz +0 -0
  24. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  25. package/dist/web/public/index.html +18 -7
  26. package/dist/web/public/index.html.br +0 -0
  27. package/dist/web/public/index.html.gz +0 -0
  28. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  29. package/dist/web/public/keyboard-accessory.9936227d.js.gz +0 -0
  30. package/dist/web/public/mobile-handlers.32fdd57f.js.gz +0 -0
  31. package/dist/web/public/mobile-overview.js.gz +0 -0
  32. package/dist/web/public/mobile.32cacde5.css.gz +0 -0
  33. package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
  34. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  35. package/dist/web/public/panels-ui.1d4203e0.js.gz +0 -0
  36. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  37. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  38. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  39. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  40. package/dist/web/public/session-ui.1356a4f4.js.gz +0 -0
  41. package/dist/web/public/{settings-ui.ea63d5db.js → settings-ui.b41d33a5.js} +2 -2
  42. package/dist/web/public/settings-ui.b41d33a5.js.br +0 -0
  43. package/dist/web/public/settings-ui.b41d33a5.js.gz +0 -0
  44. package/dist/web/public/styles.c4e58e13.css +1 -0
  45. package/dist/web/public/styles.c4e58e13.css.br +0 -0
  46. package/dist/web/public/styles.c4e58e13.css.gz +0 -0
  47. package/dist/web/public/subagent-windows.4cc8b005.js.gz +0 -0
  48. package/dist/web/public/sw.js.gz +0 -0
  49. package/dist/web/public/{terminal-ui.d5ae6dcf.js → terminal-ui.bc6819a9.js} +1 -1
  50. package/dist/web/public/terminal-ui.bc6819a9.js.br +0 -0
  51. package/dist/web/public/{terminal-ui.d5ae6dcf.js.gz → terminal-ui.bc6819a9.js.gz} +0 -0
  52. package/dist/web/public/ultracode-panel.js.gz +0 -0
  53. package/dist/web/public/ultracode-windows.js.gz +0 -0
  54. package/dist/web/public/upload.html.gz +0 -0
  55. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  56. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  57. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  58. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  59. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  60. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  61. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  62. package/dist/web/public/vendor/xterm.css.gz +0 -0
  63. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  64. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  65. package/dist/web/public/webview-tabs.js +6 -2
  66. package/dist/web/public/webview-tabs.js.br +0 -0
  67. package/dist/web/public/webview-tabs.js.gz +0 -0
  68. package/dist/web/routes/session-routes.d.ts.map +1 -1
  69. package/dist/web/routes/session-routes.js +16 -1
  70. package/dist/web/routes/session-routes.js.map +1 -1
  71. package/dist/web/routes/webview-routes.d.ts.map +1 -1
  72. package/dist/web/routes/webview-routes.js +50 -3
  73. package/dist/web/routes/webview-routes.js.map +1 -1
  74. package/dist/web/schemas.d.ts +1 -0
  75. package/dist/web/schemas.d.ts.map +1 -1
  76. package/dist/web/schemas.js +8 -0
  77. package/dist/web/schemas.js.map +1 -1
  78. package/dist/web/server.d.ts +1 -0
  79. package/dist/web/server.d.ts.map +1 -1
  80. package/dist/web/server.js +7 -0
  81. package/dist/web/server.js.map +1 -1
  82. package/package.json +1 -1
  83. package/skills/codeman/SKILL.md +94 -24
  84. package/skills/codeman/reference/endpoints.md +21 -6
  85. package/skills/codeman/reference/recipes.md +55 -25
  86. package/dist/web/public/app.b24f795e.js.br +0 -0
  87. package/dist/web/public/app.b24f795e.js.gz +0 -0
  88. package/dist/web/public/settings-ui.ea63d5db.js.br +0 -0
  89. package/dist/web/public/settings-ui.ea63d5db.js.gz +0 -0
  90. package/dist/web/public/styles.b10e5edd.css +0 -1
  91. package/dist/web/public/styles.b10e5edd.css.br +0 -0
  92. package/dist/web/public/styles.b10e5edd.css.gz +0 -0
  93. package/dist/web/public/terminal-ui.d5ae6dcf.js.br +0 -0
@@ -17,7 +17,24 @@ sessions. Every recipe below was verified live. Full endpoint tables and
17
17
  troubleshooting: [reference/endpoints.md](reference/endpoints.md). Worked multi-worker
18
18
  flows: [reference/recipes.md](reference/recipes.md).
19
19
 
20
- ## 0. Guard — run this before anything else
20
+ ## 0. Guard, and the one thing that breaks every recipe below
21
+
22
+ ⚠️ **Your shell state does not survive between tool calls.** Each Bash call starts a
23
+ fresh shell, so `$API`, `$SELF`, the `CURL` array and `delete_session` are all gone by
24
+ the next call, and `$$` is a different pid. Three consequences, all of which have
25
+ teeth:
26
+
27
+ - **Re-run this entire preamble at the top of every Bash call that touches the API.**
28
+ Running it once and assuming it stuck is the single most likely way to break a run.
29
+ - **Never re-paste only half of it.** The delete guard below is written so that a
30
+ missing definition deletes nothing, but that only holds if you never hand-roll a
31
+ `DELETE` of your own.
32
+ - **Never put `$$` in a `clientId`.** It changes per call, so the "resend the identical
33
+ request" loop in §3 would stop being a duplicate and would **retype the prompt**,
34
+ submitting the turn twice. Use a fixed literal (`codeman-agent-1` below).
35
+
36
+ Only real environment variables (`CODEMAN_*`) survive, which is why this preamble
37
+ rebuilds everything else from them.
21
38
 
22
39
  ```bash
23
40
  test "${CODEMAN_MUX:-}" = 1 || { echo "Not inside a Codeman-managed session; refusing to act."; exit 1; }
@@ -39,13 +56,35 @@ if [ -z "${CODEMAN_PASSWORD:-}" ]; then # stock installs: install.sh puts it
39
56
  UNIT="$HOME/.config/systemd/user/codeman-web.service"
40
57
  PLIST="$HOME/Library/LaunchAgents/com.codeman.web.plist"
41
58
  if [ -f "$UNIT" ]; then
42
- CODEMAN_PASSWORD=$(sed -n 's/^Environment="CODEMAN_PASSWORD=\(.*\)"$/\1/p' "$UNIT" | head -1)
59
+ # install.sh backslash-escapes " and \ in the unit value; undo it or a password
60
+ # containing either recovers wrong and auth fails.
61
+ CODEMAN_PASSWORD=$(sed -n 's/^Environment="CODEMAN_PASSWORD=\(.*\)"$/\1/p' "$UNIT" | head -1 | sed 's/\\\(["\\]\)/\1/g')
43
62
  elif [ -f "$PLIST" ]; then
44
- CODEMAN_PASSWORD=$(awk '/<key>CODEMAN_PASSWORD<\/key>/{getline; print}' "$PLIST" | sed -n 's/.*<string>\(.*\)<\/string>.*/\1/p')
63
+ # install.sh XML-escapes the plist value; undo it (&amp; LAST, mirroring escape order).
64
+ CODEMAN_PASSWORD=$(awk '/<key>CODEMAN_PASSWORD<\/key>/{getline; print}' "$PLIST" | sed -n 's/.*<string>\(.*\)<\/string>.*/\1/p' \
65
+ | sed -e 's/&lt;/</g' -e 's/&gt;/>/g' -e 's/&amp;/\&/g')
45
66
  fi
46
67
  fi
47
68
  AUTH=(); [ -n "${CODEMAN_PASSWORD:-}" ] && AUTH=(-u "${CODEMAN_USERNAME:-admin}:$CODEMAN_PASSWORD")
48
69
  CURL=(curl -sk "${AUTH[@]}") # -k: harmless on http, required on https (self-signed cert)
70
+
71
+ # Fail-CLOSED session delete. The DELETE lives INSIDE the guard on purpose: the older
72
+ # `is_self "$SID" || curl -X DELETE ...` shape failed OPEN, because an undefined
73
+ # is_self exits 127 and the `||` branch then ran the delete completely unguarded.
74
+ # Undefined delete_session is "command not found", which deletes nothing.
75
+ delete_session() {
76
+ local id="${1:-}"
77
+ [ -n "$id" ] || { echo "refusing: empty session id"; return 1; }
78
+ [ "${#SELF}" -ge 8 ] || { echo "refusing: \$SELF unset or too short to prove this is not me"; return 1; }
79
+ # ids appear in full AND 8-char form (Docker exports a truncated $SELF; mux names and
80
+ # UI surfaces carry 8-char ids), so compare by prefix in BOTH directions. Equality or
81
+ # a one-directional check each miss a real combination, and the miss deletes you.
82
+ case "$id" in "$SELF"*) echo "refusing: $id is me"; return 1 ;; esac
83
+ case "$SELF" in "$id"*) echo "refusing: $id is me"; return 1 ;; esac
84
+ "${CURL[@]}" -X DELETE "$API/api/v1/sessions/$id"
85
+ }
86
+
87
+ CID=codeman-agent-1 # FIXED literal, never "agent-$$" (see §0)
49
88
  ```
50
89
 
51
90
  - If `CODEMAN_MUX` is not `1`, **stop and say so**. Do not guess an API URL; a server
@@ -67,20 +106,16 @@ CURL=(curl -sk "${AUTH[@]}") # -k: harmless on http, required on https (self-s
67
106
 
68
107
  You are yourself a session on this server, and the API has **no undo**.
69
108
 
70
- - **Never act on your own session — and know that this check is the ONLY guard.**
109
+ - **Never act on your own session, and know that `delete_session` is the ONLY guard.**
71
110
  The server has no self-protection: a session that DELETEs its own id succeeds and
72
- dies silently (verified live). Session ids appear in both full and 8-character
73
- forms (Docker cases export a truncated `$SELF`; mux names and UI surfaces carry
74
- 8-char ids), so compare by prefix **in both directions**, never by equality:
75
-
76
- ```bash
77
- is_self() { case "$1" in "$SELF"*) return 0 ;; esac; case "$SELF" in "$1"*) return 0 ;; esac; return 1; }
78
- ```
79
-
80
- One-directional or equality checks each miss a real combination (full `$SELF` vs
81
- a target you transcribed in 8-char form, or truncated `$SELF` vs a full target)
82
- and the miss deletes you. Check `is_self` before every `DELETE`, kill, respawn,
83
- or input call.
111
+ dies silently (verified live). **Always delete through `delete_session "$SID"` from
112
+ §0; never write a bare `curl -X DELETE` and never reintroduce the
113
+ `is_self … || curl -X DELETE …` shape.** That older form failed open: with the
114
+ function undefined (a half-re-pasted preamble, see §0) bash returns 127, the `||`
115
+ branch fires, and the delete runs with no self-check at all. Wrapping the request
116
+ inside the guard is what makes a lost preamble delete nothing instead of deleting
117
+ you. Apply the same prefix-both-directions reasoning before any kill, respawn, or
118
+ input call you write by hand.
84
119
  - **Mutating calls you may make unprompted** (this is an allowlist):
85
120
  `POST /api/v1/quick-start`, `POST /api/v1/sessions/:id/input`, and
86
121
  `DELETE /api/v1/sessions/:id` **only** for a session you created in this
@@ -162,15 +197,25 @@ the composer is not) and always pays it in full before the fallback runs — the
162
197
  budget belongs to stage 3, after the dialog is answered:
163
198
 
164
199
  ```bash
165
- SID=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
166
- -d '{"caseName":"worker-1","mode":"claude"}' | jq -r '.data.sessionId')
200
+ # ALWAYS check .success: on failure `.data.sessionId` is null, jq -r prints the string
201
+ # "null", and the flow below then burns its full readiness budget against
202
+ # /api/v1/sessions/null before reporting jq noise instead of the actual cause.
203
+ Q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
204
+ -d '{"caseName":"worker-1","mode":"claude"}')
205
+ SID=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$Q")
206
+ if [ -z "$SID" ]; then
207
+ # SESSION_BUSY here is the 50-session cap, not the waiter cap; FORBIDDEN/CONFLICT/
208
+ # OPERATION_FAILED/INVALID_INPUT are the others. None are retryable in a loop.
209
+ jq -c '{error, errorCode}' <<<"$Q"; echo "quick-start failed; stopping."
210
+ exit 1
211
+ fi
167
212
  for _ in $(seq 1 30); do # bounded: a bad SID would otherwise poll forever
168
213
  [ "$("${CURL[@]}" "$API/api/v1/sessions/$SID" | jq '.data.pid')" != null ] && break; sleep 1
169
214
  done
170
215
  # ⚠️ pid != null proves STARTUP only, never life: a worker that later dies inside
171
216
  # its pane keeps status "idle" and a pid (the local tmux attach client, not the
172
217
  # worker). The death check is wait?until=exit, below.
173
- CID="agent-$$"; SEQ=1
218
+ SEQ=1 # $CID came from the §0 preamble; do NOT rebuild it from $$
174
219
  # the composer's status bar ("bypass permissions on") is the ready marker — Codeman
175
220
  # spawns claude in bypass mode. Single-token matches only: TUI text is space-less.
176
221
  R=$("${CURL[@]}" -G "$API/api/v1/sessions/$SID/wait-output" \
@@ -245,15 +290,40 @@ SEQ=$((SEQ+1))
245
290
  The typed line shows `${M}_…`, the real output shows `DONE_… rc=<exit code>`, and the
246
291
  snippet carries the exit code back to you.
247
292
 
248
- **Read a worker's output** — the terminal buffer, tail in **bytes** (`textOutput` in
249
- `GET .../output` stays empty for interactive sessions; don't use it):
293
+ **Read a worker's answer.** For `claude` and `codex` workers this is the read path:
294
+ `last-response` returns the agent's final message as clean text, taken from the
295
+ transcript rather than the screen, so it carries none of the TUI's box-drawing or
296
+ repaint noise.
297
+
298
+ ```bash
299
+ for _ in $(seq 1 10); do # the transcript write LAGS the stop signal
300
+ TXT=$("${CURL[@]}" "$API/api/v1/sessions/$SID/last-response" | jq -r '.data.text')
301
+ [ -n "$TXT" ] && break; sleep 1
302
+ done
303
+ printf '%s\n' "$TXT"
304
+ ```
305
+
306
+ `.data` is `{text, timestamp}`. ⚠️ **Poll it, do not read it once.** `text` is written
307
+ from the transcript file, which is flushed slightly *after* the `stop` hook fires, so a
308
+ single read taken the instant send-and-wait returns comes back `""` even though the
309
+ turn finished (verified live: empty on the first call, full text seconds later). `text`
310
+ is also `""` before the worker's first completed turn, and always `""` for modes with
311
+ no transcript (`shell`, `opencode`, `gemini`, `antigravity`, verified live), which is
312
+ why the loop above is bounded rather than open-ended. Fall back to the terminal buffer there, tail in **bytes**
313
+ (`textOutput` in `GET .../output` stays empty for interactive sessions; don't use it):
250
314
 
251
315
  ```bash
252
316
  "${CURL[@]}" "$API/api/v1/sessions/$SID/terminal?tail=3000" | jq -r '.data.terminalBuffer' \
253
317
  | sed -e 's/\x1b\[[0-9;?]*[a-zA-Z]//g' -e 's/\x1b([B0]//g' | grep -v '^[[:space:]]*$' | tail -30
254
318
  ```
255
319
 
256
- Avoid `?full=1` (entire tmux scrollback, a context bomb) unless doing a post-mortem.
320
+ ⚠️ Do not use that pipeline to read a **claude/codex** answer. A full-screen TUI draws
321
+ with cursor moves, so the stripped buffer is largely one long line: `tail -30` has
322
+ almost nothing to split on and you get a wall of repaint noise with the answer buried
323
+ in it (verified live, side by side with `last-response` returning the exact prose).
324
+ The terminal buffer is for *diagnosis* (is my prompt sitting unsubmitted?), not for
325
+ reading answers. Avoid `?full=1` (entire tmux scrollback, a context bomb) unless doing
326
+ a post-mortem.
257
327
 
258
328
  **Detect a dead worker cheaply**: `GET .../wait?until=exit&timeout=60000` answers
259
329
  immediately (`signal:"exit"`, `immediate:true`) if the PTY is gone — including a
@@ -262,10 +332,10 @@ as `status:"idle"` with a pid (that pid is the local tmux attach client, not the
262
332
  worker). The wait routes are the only liveness check; a worker dying while a wait
263
333
  is parked resolves it within ~3 s. A session deleted mid-wait resolves in ~1 s.
264
334
 
265
- **Clean up** — only ids you created, `is_self`-checked, one at a time:
335
+ **Clean up** — only ids you created, one at a time, always through the §0 helper:
266
336
 
267
337
  ```bash
268
- is_self "$SID" || "${CURL[@]}" -X DELETE "$API/api/v1/sessions/$SID"
338
+ delete_session "$SID"
269
339
  ```
270
340
 
271
341
  Everything else (endpoint tables, per-mode signal table, error codes, capacity
@@ -14,7 +14,7 @@ Every JSON response: `{"success":true,"data":…}` or
14
14
  | `INVALID_INPUT` | 400 | malformed request; the message names the bad field |
15
15
  | `UNAUTHORIZED` | 401 | auth required or failed (send `-u user:password`). ⚠️ The 401 body is plain text, NOT this envelope — `jq` dies with a parse error, see the guard in SKILL.md |
16
16
  | `NOT_FOUND` | 404 | no such session, or one this caller does not own |
17
- | `SESSION_BUSY` | 409 | this session's waiter cap (16, combined signal+output) is full |
17
+ | `SESSION_BUSY` | 409 | on a **wait**: this session's waiter cap (16, combined signal+output) is full. On **quick-start**: the 50-session cap is full, so clean up before starting more |
18
18
  | `CONFLICT` / `ALREADY_EXISTS` | 409 | conflicts with current state |
19
19
  | `OPERATION_FAILED` | 422 | well-formed but could not be completed |
20
20
  | `RATE_LIMITED` | 429 | per-owner or process-wide waiter pool is full — back off; switching sessions will not help |
@@ -32,16 +32,19 @@ Every JSON response: `{"success":true,"data":…}` or
32
32
  | unified list incl. history | `GET /api/v1/sessions/unified` → `.data.sessions[]` (NOT `.data[]`), and it folds in transcript history from the whole machine — never use it to verify cleanup; `GET /api/v1/sessions` is the cleanup check |
33
33
  | start case + session in one call | `POST /api/v1/quick-start` |
34
34
  | send input | `POST /api/v1/sessions/:id/input` |
35
- | read terminal (tail is in **BYTES**, raw ANSI) | `GET /api/v1/sessions/:id/terminal?tail=3000` → `.data.terminalBuffer` |
35
+ | **read a worker's answer** (claude/codex) | `GET /api/v1/sessions/:id/last-response` → `.data.{text,timestamp}` — clean transcript text, no TUI noise. ⚠️ **Poll it**: the transcript flush lags the `stop` signal, so a read taken the instant send-and-wait returns is `""` (verified live). Also `""` before the first completed turn, and always `""` for `shell`/`opencode`/`gemini`/`antigravity` (no transcript) |
36
+ | read terminal (tail is in **BYTES**, raw ANSI) | `GET /api/v1/sessions/:id/terminal?tail=3000` → `.data.terminalBuffer` — for *diagnosis* (unsubmitted prompt?), not for reading answers |
36
37
  | full tmux scrollback (context bomb; post-mortems only) | `GET /api/v1/sessions/:id/terminal?full=1` |
37
- | background agents of a session | `GET /api/v1/subagents` |
38
+ | background agents, one session | `GET /api/v1/sessions/:id/subagents` |
39
+ | background agents, global list | `GET /api/v1/subagents` (admin-only in multi-user mode) |
38
40
  | server status / version | `GET /api/v1/status` → `.data.version` |
39
- | delete one session (yours, `is_self`-checked) | `DELETE /api/v1/sessions/:id` |
41
+ | delete one session (yours only, via `delete_session`) | `DELETE /api/v1/sessions/:id` — never call it bare; the fail-closed helper in SKILL.md §0 is the only self-protection that exists |
40
42
 
41
43
  ⚠️ `GET /api/v1/sessions/:id/output` → `.data.textOutput` looks like the obvious read
42
44
  but stays **empty for interactive tmux-backed sessions** (it is fed only by the legacy
43
- JSON-stream path). Verified empty on live claude and shell sessions. Read
44
- `terminal?tail=` instead and strip ANSI:
45
+ JSON-stream path). Verified empty on live claude and shell sessions. Use
46
+ `last-response` for claude/codex answers; only fall back to `terminal?tail=` for
47
+ hook-less modes, or to diagnose a prompt that was never submitted, and strip ANSI:
45
48
 
46
49
  ```bash
47
50
  … | jq -r '.data.terminalBuffer' | sed -e 's/\x1b\[[0-9;?]*[a-zA-Z]//g' -e 's/\x1b([B0]//g'
@@ -53,6 +56,18 @@ JSON-stream path). Verified empty on live claude and shell sessions. Read
53
56
  `.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory
54
57
  on the user's disk) if missing — do not retry it in a loop, and remember the name.
55
58
 
59
+ ⚠️ **Branch on `.success` before reading `.data.sessionId`.** On any failure the field
60
+ is absent, `jq -r` prints the literal string `null`, and every later call then targets
61
+ `/api/v1/sessions/null`, burning the full readiness budget and reporting jq noise
62
+ instead of the real cause. Failure modes here are `SESSION_BUSY` (the **50-session
63
+ cap**, not the waiter cap), `FORBIDDEN`, `CONFLICT`, `OPERATION_FAILED` and
64
+ `INVALID_INPUT`; none of them are retryable in a loop.
65
+
66
+ ⚠️ `caseName` resolves through the linked-cases registry first, so a name that happens
67
+ to match a case the user linked in lands in that **real repo**, not a fresh scratch
68
+ directory. Pick distinctive scratch names, and use a linked name deliberately when you
69
+ do want a worker in an existing checkout.
70
+
56
71
  `POST /api/v1/sessions/:id/input` body:
57
72
  `{"input":"one line\r","useMux":true,"clientId":"agent-1","seq":1}` plus optionally
58
73
  `"wait"` / `"waitTimeout"` (below).
@@ -1,10 +1,17 @@
1
1
  # Worked orchestration flows
2
2
 
3
- Loaded on demand from the `codeman` skill. Every flow assumes the guard preamble from
4
- SKILL.md ran (`$API`, `$SELF`, `"${CURL[@]}"`, `is_self`). Track every session id you
5
- create; delete them (and only them) when done. Remember the two silent killers:
6
- **every input ends with `\r`**, and **markers must be split** so the typed-line echo
7
- does not match them.
3
+ Loaded on demand from the `codeman` skill. Every flow assumes the SKILL.md §0 preamble
4
+ is in scope (`$API`, `$SELF`, `$CID`, `"${CURL[@]}"`, `delete_session`).
5
+
6
+ ⚠️ **That preamble does not survive between tool calls**, so re-run it at the top of
7
+ every Bash call that uses these flows, in full. Re-pasting only part of it is the
8
+ failure mode the fail-closed `delete_session` exists to contain, and a `clientId` you
9
+ rebuild from `$$` changes per call, which turns the duplicate-resend loop in Flow 1
10
+ into a second typed prompt.
11
+
12
+ Track every session id you create; delete them (and only them) when done. The two
13
+ silent killers: **every input ends with `\r`**, and **markers must be split** so the
14
+ typed-line echo does not match them.
8
15
 
9
16
  ## Flow 1: claude worker, end to end
10
17
 
@@ -13,11 +20,15 @@ the turn to finish, read the answer, clean up. Verified live: the stop hook reso
13
20
  the send-and-wait within seconds of the turn ending.
14
21
 
15
22
  ```bash
16
- # 1. start (returns before the CLI inside is ready)
17
- SID=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
18
- -d '{"caseName":"worker-tests","mode":"claude"}' | jq -r '.data.sessionId')
23
+ # 1. start (returns before the CLI inside is ready). ALWAYS check .success: on failure
24
+ # .data.sessionId is null, jq -r yields the string "null", and every step below
25
+ # then runs against /api/v1/sessions/null and reports jq noise, not the cause.
26
+ Q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
27
+ -d '{"caseName":"worker-tests","mode":"claude"}')
28
+ SID=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$Q")
29
+ [ -n "$SID" ] || { jq -c '{error, errorCode}' <<<"$Q"; echo "quick-start failed"; exit 1; }
19
30
  CREATED+=("$SID") # the cleanup list
20
- CID="agent-$$"; SEQ=1
31
+ SEQ=1 # $CID is the fixed literal from §0; never rebuild it from $$
21
32
 
22
33
  # 2. readiness. "wait for idle" or "wait for ❯" is NOT readiness: a fresh session
23
34
  # reports idle before anything spawned, and the first-run trust dialog contains ❯.
@@ -84,13 +95,23 @@ case "$(jq -r '.data.wait.signal' <<<"$R")" in
84
95
  null) jq -e '.data.wait.ended' <<<"$R" >/dev/null && echo "worker deleted mid-wait" ;;
85
96
  esac
86
97
 
87
- # 5. read the answer: terminal tail (BYTES), ANSI-stripped. textOutput stays empty
88
- # for interactive sessions; terminal?full=1 is a context bomb.
89
- "${CURL[@]}" "$API/api/v1/sessions/$SID/terminal?tail=4000" | jq -r '.data.terminalBuffer' \
90
- | sed -e 's/\x1b\[[0-9;?]*[a-zA-Z]//g' -e 's/\x1b([B0]//g' | grep -v '^[[:space:]]*$' | tail -30
98
+ # 5. read the answer. For a claude worker this is last-response: clean transcript text,
99
+ # no TUI repaint noise. Do NOT scrape the terminal for this — a full-screen TUI
100
+ # draws with cursor moves, so the stripped buffer is nearly one long line and the
101
+ # answer arrives buried in redraw garbage.
102
+ # POLL it: the transcript flush lags the stop signal, so a single read taken the
103
+ # instant step 3 returned comes back "" even though the turn finished (verified live).
104
+ for _ in $(seq 1 10); do
105
+ TXT=$("${CURL[@]}" "$API/api/v1/sessions/$SID/last-response" | jq -r '.data.text')
106
+ [ -n "$TXT" ] && break; sleep 1
107
+ done
108
+ printf '%s\n' "$TXT"
109
+ # (.data is {text,timestamp}; text is also "" before the first completed turn and
110
+ # always "" for shell/opencode/gemini/antigravity, which have no transcript — use
111
+ # the terminal tail there, and here only to diagnose an unsubmitted prompt.)
91
112
 
92
- # 6. clean up — exact id, own list only, self-check
93
- is_self "$SID" || "${CURL[@]}" -X DELETE "$API/api/v1/sessions/$SID"
113
+ # 6. clean up — exact id, own list only, through the fail-closed §0 helper
114
+ delete_session "$SID"
94
115
  ```
95
116
 
96
117
  Increment `SEQ` for every *new* input to the same worker. Reuse the same `SEQ` only to
@@ -104,8 +125,10 @@ live), so send-and-wait can burn its whole timeout. The reliable pattern is a sp
104
125
  unique marker plus `wait-output from=buffer`:
105
126
 
106
127
  ```bash
107
- SID=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
108
- -d '{"caseName":"builder","mode":"shell"}' | jq -r '.data.sessionId')
128
+ Q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
129
+ -d '{"caseName":"builder","mode":"shell"}')
130
+ SID=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$Q")
131
+ [ -n "$SID" ] || { jq -c '{error, errorCode}' <<<"$Q"; echo "quick-start failed"; exit 1; }
109
132
  CREATED+=("$SID")
110
133
  for _ in $(seq 1 30); do
111
134
  [ "$("${CURL[@]}" "$API/api/v1/sessions/$SID" | jq '.data.pid')" != null ] && break; sleep 1
@@ -115,7 +138,7 @@ done
115
138
  # An unsplit marker matches the echo of your own keystrokes before the build runs.
116
139
  N="${RANDOM}_$$"; MARK="DONE_$N"
117
140
  "${CURL[@]}" -X POST "$API/api/v1/sessions/$SID/input" -H 'Content-Type: application/json' \
118
- -d '{"input":"M=DONE; npm run build; echo ${M}_'"$N"' rc=$?\r","useMux":true,"clientId":"build-'$$'","seq":1}'
141
+ -d '{"input":"M=DONE; npm run build; echo ${M}_'"$N"' rc=$?\r","useMux":true,"clientId":"codeman-build-1","seq":1}'
119
142
 
120
143
  for TRY in $(seq 1 30); do # BOUNDED (30 min): a \r-less send makes an uncapped loop infinite
121
144
  R=$("${CURL[@]}" -G "$API/api/v1/sessions/$SID/wait-output" \
@@ -136,8 +159,10 @@ waiter cap is 16 and abandoned concurrent waits pile up against it.
136
159
  ```bash
137
160
  declare -A WORKER MARKS
138
161
  for task in lint typecheck unit; do
139
- SID=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
140
- -d '{"caseName":"fan-'"$task"'","mode":"shell"}' | jq -r '.data.sessionId')
162
+ Q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
163
+ -d '{"caseName":"fan-'"$task"'","mode":"shell"}')
164
+ SID=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$Q")
165
+ [ -n "$SID" ] || { jq -c '{error, errorCode}' <<<"$Q"; echo "$task: spawn failed"; continue; }
141
166
  WORKER[$task]=$SID; CREATED+=("$SID")
142
167
  done
143
168
  for task in "${!WORKER[@]}"; do
@@ -147,7 +172,7 @@ for task in "${!WORKER[@]}"; do
147
172
  done
148
173
  N="${task}_${RANDOM}"; MARKS[$task]="DONE_$N"
149
174
  "${CURL[@]}" -X POST "$API/api/v1/sessions/$SID/input" -H 'Content-Type: application/json' \
150
- -d '{"input":"M=DONE; npm run '"$task"'; echo ${M}_'"$N"' rc=$?\r","useMux":true,"clientId":"fan-'$$'","seq":1}'
175
+ -d '{"input":"M=DONE; npm run '"$task"'; echo ${M}_'"$N"' rc=$?\r","useMux":true,"clientId":"codeman-fan-'"$task"'","seq":1}'
151
176
  done
152
177
  for task in "${!WORKER[@]}"; do # sequential gather; each wait blocks until that worker is done
153
178
  for TRY in $(seq 1 30); do # BOUNDED per worker, same reasoning as Flow 2
@@ -171,8 +196,8 @@ other was still running):
171
196
 
172
197
  ```bash
173
198
  sendwait() { # $1=sid $2=prompt $3=seq — assumes the worker passed Flow 1's readiness
174
- local body; body=$(jq -n --arg p "$2" --argjson s "$3" \
175
- '{input:($p+"\r"),useMux:true,clientId:"fan-'$$'",seq:$s,wait:true,waitTimeout:600000}')
199
+ local body; body=$(jq -n --arg p "$2" --argjson s "$3" --arg c "codeman-fan-$1" \
200
+ '{input:($p+"\r"),useMux:true,clientId:$c,seq:$s,wait:true,waitTimeout:600000}')
176
201
  "${CURL[@]}" -X POST "$API/api/v1/sessions/$1/input" \
177
202
  -H 'Content-Type: application/json' --data-binary "$body" > "/tmp/fan-$1.json"
178
203
  }
@@ -200,7 +225,7 @@ declare -A TOK
200
225
  for i in 1 2; do
201
226
  TOK[$i]="${RANDOM}_$i"
202
227
  BODY=$(jq -n --arg p "do task $i; when completely done print the word WORKDONE immediately followed by _${TOK[$i]}" \
203
- --arg c "fan-$$" --argjson s 2 '{input:($p+"\r"),useMux:true,clientId:$c,seq:$s}')
228
+ --arg c "codeman-fan-$i" --argjson s 2 '{input:($p+"\r"),useMux:true,clientId:$c,seq:$s}')
204
229
  "${CURL[@]}" -X POST "$API/api/v1/sessions/${SIDS[$i]}/input" \
205
230
  -H 'Content-Type: application/json' --data-binary "$BODY"
206
231
  done
@@ -237,12 +262,17 @@ At the end of the conversation (or on abort), delete exactly what you created:
237
262
 
238
263
  ```bash
239
264
  for id in "${CREATED[@]}"; do
240
- is_self "$id" || "${CURL[@]}" -X DELETE "$API/api/v1/sessions/$id"
265
+ delete_session "$id"
241
266
  done
242
267
  ```
243
268
 
244
269
  - Only ids from your own `CREATED` list. Never enumerate `/api/v1/sessions` and
245
270
  delete by pattern; other sessions belong to the user.
271
+ - Always go through `delete_session`. It refuses an empty id, refuses when `$SELF` is
272
+ unset or too short to prove the target is not you, and prefix-checks in both
273
+ directions. A hand-written `curl -X DELETE`, or the old
274
+ `is_self "$id" || curl -X DELETE …`, has none of that: an undefined `is_self` exits
275
+ 127 and the `||` branch deletes unguarded.
246
276
  - If you created a *case* purely as scratch and the user confirmed it is disposable,
247
277
  `DELETE /api/v1/cases/:name` removes it — but that recursively deletes the
248
278
  directory from disk, so never do it without the user's explicit go-ahead for that
Binary file
Binary file