aicodeman 1.22.0 → 1.23.1

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 (205) hide show
  1. package/README.md +1 -1
  2. package/dist/config/dependency-registry.d.ts.map +1 -1
  3. package/dist/config/dependency-registry.js +27 -0
  4. package/dist/config/dependency-registry.js.map +1 -1
  5. package/dist/cron/cron-service.d.ts +3 -2
  6. package/dist/cron/cron-service.d.ts.map +1 -1
  7. package/dist/cron/cron-service.js +17 -3
  8. package/dist/cron/cron-service.js.map +1 -1
  9. package/dist/deepseek-status-shim.d.ts +77 -0
  10. package/dist/deepseek-status-shim.d.ts.map +1 -0
  11. package/dist/deepseek-status-shim.js +268 -0
  12. package/dist/deepseek-status-shim.js.map +1 -0
  13. package/dist/deepseek-transcript.d.ts +188 -0
  14. package/dist/deepseek-transcript.d.ts.map +1 -0
  15. package/dist/deepseek-transcript.js +633 -0
  16. package/dist/deepseek-transcript.js.map +1 -0
  17. package/dist/deepseek-web-server.d.ts +60 -0
  18. package/dist/deepseek-web-server.d.ts.map +1 -0
  19. package/dist/deepseek-web-server.js +249 -0
  20. package/dist/deepseek-web-server.js.map +1 -0
  21. package/dist/docker-hosts.d.ts.map +1 -1
  22. package/dist/docker-hosts.js +12 -0
  23. package/dist/docker-hosts.js.map +1 -1
  24. package/dist/mux-interface.d.ts +3 -1
  25. package/dist/mux-interface.d.ts.map +1 -1
  26. package/dist/remote-hosts.d.ts.map +1 -1
  27. package/dist/remote-hosts.js +4 -0
  28. package/dist/remote-hosts.js.map +1 -1
  29. package/dist/session.d.ts +27 -1
  30. package/dist/session.d.ts.map +1 -1
  31. package/dist/session.js +47 -2
  32. package/dist/session.js.map +1 -1
  33. package/dist/tmux-manager.d.ts +23 -1
  34. package/dist/tmux-manager.d.ts.map +1 -1
  35. package/dist/tmux-manager.js +134 -5
  36. package/dist/tmux-manager.js.map +1 -1
  37. package/dist/tui/tui-app.d.ts.map +1 -1
  38. package/dist/tui/tui-app.js +1 -0
  39. package/dist/tui/tui-app.js.map +1 -1
  40. package/dist/tui/tui-client.d.ts +1 -1
  41. package/dist/tui/tui-client.d.ts.map +1 -1
  42. package/dist/types/api.d.ts +1 -1
  43. package/dist/types/api.d.ts.map +1 -1
  44. package/dist/types/api.js.map +1 -1
  45. package/dist/types/session.d.ts +61 -4
  46. package/dist/types/session.d.ts.map +1 -1
  47. package/dist/types/session.js +2 -1
  48. package/dist/types/session.js.map +1 -1
  49. package/dist/types/webview.d.ts +15 -0
  50. package/dist/types/webview.d.ts.map +1 -1
  51. package/dist/utils/deepseek-cli-resolver.d.ts +144 -0
  52. package/dist/utils/deepseek-cli-resolver.d.ts.map +1 -0
  53. package/dist/utils/deepseek-cli-resolver.js +339 -0
  54. package/dist/utils/deepseek-cli-resolver.js.map +1 -0
  55. package/dist/utils/index.d.ts +2 -0
  56. package/dist/utils/index.d.ts.map +1 -1
  57. package/dist/utils/index.js +1 -0
  58. package/dist/utils/index.js.map +1 -1
  59. package/dist/web/public/admin-ui.js.gz +0 -0
  60. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  61. package/dist/web/public/app.c5cf1b8a.js +36 -0
  62. package/dist/web/public/app.c5cf1b8a.js.br +0 -0
  63. package/dist/web/public/app.c5cf1b8a.js.gz +0 -0
  64. package/dist/web/public/approvals-ui.js.gz +0 -0
  65. package/dist/web/public/{constants.189c7bff.js → constants.f4ea0a19.js} +8 -1
  66. package/dist/web/public/constants.f4ea0a19.js.br +0 -0
  67. package/dist/web/public/constants.f4ea0a19.js.gz +0 -0
  68. package/dist/web/public/cron-ui.js.gz +0 -0
  69. package/dist/web/public/entrance-animations.js.gz +0 -0
  70. package/dist/web/public/home-sessions.js +1 -0
  71. package/dist/web/public/home-sessions.js.br +0 -0
  72. package/dist/web/public/home-sessions.js.gz +0 -0
  73. package/dist/web/public/i18n.9ae15b51.js +1 -0
  74. package/dist/web/public/i18n.9ae15b51.js.br +0 -0
  75. package/dist/web/public/i18n.9ae15b51.js.gz +0 -0
  76. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  77. package/dist/web/public/index.html +47 -14
  78. package/dist/web/public/index.html.br +0 -0
  79. package/dist/web/public/index.html.gz +0 -0
  80. package/dist/web/public/input-cjk.87f14251.js.gz +0 -0
  81. package/dist/web/public/keyboard-accessory.0454a3e9.js.gz +0 -0
  82. package/dist/web/public/mobile-handlers.0f83a608.js.gz +0 -0
  83. package/dist/web/public/mobile-overview.js +6 -0
  84. package/dist/web/public/mobile-overview.js.br +0 -0
  85. package/dist/web/public/mobile-overview.js.gz +0 -0
  86. package/dist/web/public/mobile.cab92e3f.css +1 -0
  87. package/dist/web/public/mobile.cab92e3f.css.br +0 -0
  88. package/dist/web/public/mobile.cab92e3f.css.gz +0 -0
  89. package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
  90. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  91. package/dist/web/public/{panels-ui.7c667123.js → panels-ui.344de87d.js} +41 -33
  92. package/dist/web/public/panels-ui.344de87d.js.br +0 -0
  93. package/dist/web/public/panels-ui.344de87d.js.gz +0 -0
  94. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  95. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  96. package/dist/web/public/readmymind-ui.js.gz +0 -0
  97. package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
  98. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  99. package/dist/web/public/session-lineage.js.gz +0 -0
  100. package/dist/web/public/session-ui.103656d3.js +68 -0
  101. package/dist/web/public/session-ui.103656d3.js.br +0 -0
  102. package/dist/web/public/session-ui.103656d3.js.gz +0 -0
  103. package/dist/web/public/settings-ui.3261b896.js +67 -0
  104. package/dist/web/public/settings-ui.3261b896.js.br +0 -0
  105. package/dist/web/public/settings-ui.3261b896.js.gz +0 -0
  106. package/dist/web/public/styles.1938fd02.css +1 -0
  107. package/dist/web/public/styles.1938fd02.css.br +0 -0
  108. package/dist/web/public/styles.1938fd02.css.gz +0 -0
  109. package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
  110. package/dist/web/public/sw.js.gz +0 -0
  111. package/dist/web/public/tab-rail-resize.42c24949.js +1 -0
  112. package/dist/web/public/tab-rail-resize.42c24949.js.br +0 -0
  113. package/dist/web/public/tab-rail-resize.42c24949.js.gz +0 -0
  114. package/dist/web/public/terminal-ui.5e0c230c.js +2 -0
  115. package/dist/web/public/terminal-ui.5e0c230c.js.br +0 -0
  116. package/dist/web/public/terminal-ui.5e0c230c.js.gz +0 -0
  117. package/dist/web/public/ultracode-panel.js.gz +0 -0
  118. package/dist/web/public/ultracode-windows.js.gz +0 -0
  119. package/dist/web/public/upload.html.gz +0 -0
  120. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  121. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  122. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  123. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  124. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  125. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  126. package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
  127. package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
  128. package/dist/web/public/vendor/xterm.css.gz +0 -0
  129. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  130. package/dist/web/public/voice-input.4ec670b0.js.gz +0 -0
  131. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  132. package/dist/web/public/webview-tabs.js +4 -1
  133. package/dist/web/public/webview-tabs.js.br +0 -0
  134. package/dist/web/public/webview-tabs.js.gz +0 -0
  135. package/dist/web/response-viewer-transcript.js +1 -1
  136. package/dist/web/response-viewer-transcript.js.map +1 -1
  137. package/dist/web/routes/approval-routes.d.ts.map +1 -1
  138. package/dist/web/routes/approval-routes.js +12 -2
  139. package/dist/web/routes/approval-routes.js.map +1 -1
  140. package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
  141. package/dist/web/routes/hook-event-routes.js +51 -7
  142. package/dist/web/routes/hook-event-routes.js.map +1 -1
  143. package/dist/web/routes/readmymind-routes.d.ts.map +1 -1
  144. package/dist/web/routes/readmymind-routes.js +5 -2
  145. package/dist/web/routes/readmymind-routes.js.map +1 -1
  146. package/dist/web/routes/session-routes.d.ts +30 -2
  147. package/dist/web/routes/session-routes.d.ts.map +1 -1
  148. package/dist/web/routes/session-routes.js +158 -16
  149. package/dist/web/routes/session-routes.js.map +1 -1
  150. package/dist/web/routes/system-routes.d.ts.map +1 -1
  151. package/dist/web/routes/system-routes.js +203 -2
  152. package/dist/web/routes/system-routes.js.map +1 -1
  153. package/dist/web/routes/webview-routes.d.ts.map +1 -1
  154. package/dist/web/routes/webview-routes.js +1 -0
  155. package/dist/web/routes/webview-routes.js.map +1 -1
  156. package/dist/web/schemas.d.ts +68 -0
  157. package/dist/web/schemas.d.ts.map +1 -1
  158. package/dist/web/schemas.js +107 -4
  159. package/dist/web/schemas.js.map +1 -1
  160. package/dist/web/server.d.ts.map +1 -1
  161. package/dist/web/server.js +21 -3
  162. package/dist/web/server.js.map +1 -1
  163. package/dist/web/session-wait-registry.d.ts +63 -7
  164. package/dist/web/session-wait-registry.d.ts.map +1 -1
  165. package/dist/web/session-wait-registry.js +67 -9
  166. package/dist/web/session-wait-registry.js.map +1 -1
  167. package/dist/web/sse-events.d.ts +11 -2
  168. package/dist/web/sse-events.d.ts.map +1 -1
  169. package/dist/web/sse-events.js +11 -2
  170. package/dist/web/sse-events.js.map +1 -1
  171. package/package.json +2 -1
  172. package/skills/codeman/SKILL.md +121 -34
  173. package/skills/codeman/preamble.sh +81 -26
  174. package/skills/codeman/reference/endpoints.md +18 -10
  175. package/skills/codeman/reference/messaging.md +2 -2
  176. package/skills/codeman/reference/recipes.md +53 -0
  177. package/skills/codeman/reference/verbs.md +39 -8
  178. package/dist/web/public/app.84bf8955.js +0 -36
  179. package/dist/web/public/app.84bf8955.js.br +0 -0
  180. package/dist/web/public/app.84bf8955.js.gz +0 -0
  181. package/dist/web/public/constants.189c7bff.js.br +0 -0
  182. package/dist/web/public/constants.189c7bff.js.gz +0 -0
  183. package/dist/web/public/i18n.34016b89.js +0 -1
  184. package/dist/web/public/i18n.34016b89.js.br +0 -0
  185. package/dist/web/public/i18n.34016b89.js.gz +0 -0
  186. package/dist/web/public/mobile.983f064e.css +0 -1
  187. package/dist/web/public/mobile.983f064e.css.br +0 -0
  188. package/dist/web/public/mobile.983f064e.css.gz +0 -0
  189. package/dist/web/public/panels-ui.7c667123.js.br +0 -0
  190. package/dist/web/public/panels-ui.7c667123.js.gz +0 -0
  191. package/dist/web/public/session-ui.acd79900.js +0 -68
  192. package/dist/web/public/session-ui.acd79900.js.br +0 -0
  193. package/dist/web/public/session-ui.acd79900.js.gz +0 -0
  194. package/dist/web/public/settings-ui.115a2d1e.js +0 -67
  195. package/dist/web/public/settings-ui.115a2d1e.js.br +0 -0
  196. package/dist/web/public/settings-ui.115a2d1e.js.gz +0 -0
  197. package/dist/web/public/styles.64369ebd.css +0 -1
  198. package/dist/web/public/styles.64369ebd.css.br +0 -0
  199. package/dist/web/public/styles.64369ebd.css.gz +0 -0
  200. package/dist/web/public/tab-rail-resize.f3465ce1.js +0 -1
  201. package/dist/web/public/tab-rail-resize.f3465ce1.js.br +0 -0
  202. package/dist/web/public/tab-rail-resize.f3465ce1.js.gz +0 -0
  203. package/dist/web/public/terminal-ui.ef68a666.js +0 -2
  204. package/dist/web/public/terminal-ui.ef68a666.js.br +0 -0
  205. package/dist/web/public/terminal-ui.ef68a666.js.gz +0 -0
@@ -47,7 +47,7 @@ later call opens with, and your first REAL call performs them anyway:
47
47
 
48
48
  ```bash
49
49
  . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
50
- [ "${CODEMAN_PREAMBLE:-}" = 1.19.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
50
+ [ "${CODEMAN_PREAMBLE:-}" = 1.20.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
51
51
  ```
52
52
 
53
53
  ⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same
@@ -75,8 +75,8 @@ PRE="${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh"
75
75
  mkdir -p "$(dirname "$PRE")"
76
76
  # Rewrite unless the file already ends with THIS version's stamp, so a stale or a
77
77
  # half-written file self-heals here instead of costing you a round trip to rm it.
78
- grep -qs '^CODEMAN_PREAMBLE=1.19.0$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
79
- # ---- Codeman agent preamble 1.19.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
78
+ grep -qs '^CODEMAN_PREAMBLE=1.20.0$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
79
+ # ---- Codeman agent preamble 1.20.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
80
80
  API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
81
81
  SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
82
82
  # Credentials, cheapest first. Your session has usually INHERITED the server's
@@ -121,23 +121,53 @@ _composer_up() { # <sid> <timeoutMs> -> "true"/"false". `shift+tab` is the one
121
121
  --data-urlencode 'match=shift+tab' --data-urlencode 'from=buffer' \
122
122
  --data-urlencode "timeout=$2" | jq -r '.data.wait.matched // false'
123
123
  }
124
+ _dsh_up() { # <sid> <timeoutMs> -> "true"/"false". The DeepSeek Harness TUI's
125
+ # composer glyph. Override with DSH_READY_MARK for a profile that draws another one.
126
+ "${CURL[@]}" -G "$API/api/v1/sessions/$1/wait-output" \
127
+ --data-urlencode "match=${DSH_READY_MARK:-❯}" --data-urlencode 'from=buffer' \
128
+ --data-urlencode "timeout=$2" | jq -r '.data.wait.matched // false'
129
+ }
124
130
  # spawn_worker <caseName> [mode] -> session id on stdout, diagnostics on stderr.
125
131
  # quick-start AND readiness in one call, with a strict contract: NON-EMPTY stdout means
126
- # a READY claude worker in a hook-carrying case. Anything less is rc 1 with EMPTY
127
- # stdout, and the half-spawned session is deleted here rather than handed back, because
128
- # a worker that never drew its composer would eat the task prompt with its trust
129
- # dialog. There is deliberately no pid poll: wait-output already blocks until the
130
- # composer draws, and pid!=null proved startup, never readiness.
132
+ # a READY worker whose end-of-turn signal can be trusted -- a claude worker in a
133
+ # hook-carrying case, or a `deepseek` worker whose harness TUI drew its composer.
134
+ # Anything less is rc 1 with EMPTY stdout, and the half-spawned session is deleted here
135
+ # rather than handed back, because a worker that never drew its composer would eat the
136
+ # task prompt with its trust dialog. There is deliberately no pid poll: wait-output
137
+ # already blocks until the composer draws, and pid!=null proved startup, never readiness.
131
138
  spawn_worker() {
132
139
  local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
133
140
  # parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
134
141
  # curl (or a body someone rebuilt from this recipe) still carries its lineage.
142
+ # deepseek: ask for the same permission posture the Run button sends, because the
143
+ # harness's own default (`workspace-write`) still ASKS, and a worker that stops on
144
+ # an approval row is a worker no fan-out can finish. It is not an escalation --
145
+ # claude workers already spawn with permissions skipped, and in multi-user mode the
146
+ # server clamps this back to `workspace-write` for an owner without the grant.
147
+ # Spawn by hand (§5.1) when you want a worker that asks.
135
148
  q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
136
- -d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" '{caseName:$n,mode:$m,parentSessionId:$p}')")
149
+ -d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" \
150
+ '{caseName:$n,mode:$m,parentSessionId:$p}
151
+ + (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)')")
137
152
  sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
138
153
  # NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
139
154
  [ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
140
- [ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # only claude draws a composer
155
+ if [ "$mode" = deepseek ]; then
156
+ # The one non-claude mode with REAL end-of-turn signals: its TUI reports
157
+ # idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
158
+ # Inbox all work here exactly as they do for claude. No hook file to vet
159
+ # (the bridge is env-injected, not a workspace file) and no trust dialog.
160
+ # ⚠️ Readiness is still not optional, and NOT interchangeable with the stop
161
+ # signal: the harness's boot report lands ~300ms BEFORE the composer paints
162
+ # (measured 2.26s vs 2.56s after spawn), so a sendwait fired straight after
163
+ # quick-start returns on that BOOT signal, reports a turn that never ran, and
164
+ # strands the prompt in a pane that was not yet taking input.
165
+ r=$(_dsh_up "$sid" 45000)
166
+ [ "$r" = true ] || { echo "dsh worker $sid never drew a composer: no pane-capable profile, a profile whose composer is not '${DSH_READY_MARK:-❯}' (set DSH_READY_MARK), or a harness that failed to boot -- check GET /api/v1/deepseek/status. Deleted it" >&2
167
+ delete_session "$sid" >/dev/null; return 1; }
168
+ printf '%s\n' "$sid"; return 0
169
+ fi
170
+ [ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # no other mode draws a composer to wait on
141
171
  # The server installs hooks into every claude workspace now, so this grep normally
142
172
  # passes; it stays because the install is gated on a setting the operator can turn
143
173
  # off, remote sessions never get hooks, and a session created by an older server
@@ -166,19 +196,25 @@ spawn_worker() {
166
196
  delete_session "$sid" >/dev/null; return 1; }
167
197
  printf '%s\n' "$sid"
168
198
  }
169
- # spawn_workers <caseName>... -> one "<caseName> <sessionId>" line per worker, in order;
170
- # the sessionId column is EMPTY for a spawn that failed (stderr has why). CONCURRENT:
171
- # N workers cost about what one costs. Spawning them one Bash call at a time is the
172
- # single biggest avoidable delay in this skill. Names must be UNIQUE: two workers in
173
- # one case directory co-edit the same tree (§4), so a repeat is an error here, not a race.
199
+ # spawn_workers <caseName[:mode]>... -> one "<caseName> <sessionId>" line per worker, in
200
+ # order; the sessionId column is EMPTY for a spawn that failed (stderr has why).
201
+ # CONCURRENT: N workers cost about what one costs. Spawning them one Bash call at a time
202
+ # is the single biggest avoidable delay in this skill. A bare name is a claude worker;
203
+ # `beta:deepseek` makes that one a DeepSeek Harness worker, and a mixed fleet is one
204
+ # call. Case names must be UNIQUE: two workers in one case directory co-edit the same
205
+ # tree (§4), so a repeat is an error here, not a race (the mode never disambiguates two
206
+ # workers, since they would still share the directory).
174
207
  spawn_workers() {
175
- local d n i=0
208
+ local d spec n m i=0
176
209
  [ "$#" -gt 0 ] || { echo "spawn_workers: no case names given" >&2; return 1; }
177
- [ -z "$(printf '%s\n' "$@" | sort | uniq -d)" ] || { echo "spawn_workers: duplicate case names" >&2; return 1; }
210
+ [ -z "$(printf '%s\n' "$@" | sed 's/:.*//' | sort | uniq -d)" ] || { echo "spawn_workers: duplicate case names" >&2; return 1; }
178
211
  d=$(mktemp -d "${TMPDIR:-/tmp}/codeman-spawn.XXXXXX") || return 1
179
- for n in "$@"; do ( spawn_worker "$n" > "$d/$i" ) & i=$((i+1)); done
212
+ for spec in "$@"; do
213
+ n=${spec%%:*}; m=${spec#*:}; [ "$m" = "$spec" ] && m=claude
214
+ ( spawn_worker "$n" "$m" > "$d/$i" ) & i=$((i+1))
215
+ done
180
216
  wait
181
- i=0; for n in "$@"; do printf '%s %s\n' "$n" "$(cat "$d/$i" 2>/dev/null)"; i=$((i+1)); done
217
+ i=0; for spec in "$@"; do printf '%s %s\n' "${spec%%:*}" "$(cat "$d/$i" 2>/dev/null)"; i=$((i+1)); done
182
218
  rm -rf "$d"
183
219
  }
184
220
  # sendwait <sid> <prompt> [seq] -> blocks until that worker's turn ENDS (~10 min ceiling
@@ -194,27 +230,46 @@ spawn_workers() {
194
230
  # (observed live). So the first wait is short; on its timeout a bare \r goes out (the
195
231
  # missing Enter when the prompt is stranded, a no-op when the turn is genuinely
196
232
  # running), then the ORIGINAL frame is resent unchanged, which the server takes as a
197
- # tagged duplicate: it re-waits without retyping (§5.3). Trustworthy only for a claude
198
- # worker spawn_worker handed back (hooks vetted); hook-less workspaces and other modes
199
- # resolve on flapping idle: markers instead (§5.5).
233
+ # tagged duplicate: it re-waits without retyping (§5.3). Trustworthy for a worker
234
+ # spawn_worker handed back -- claude (hooks vetted) or deepseek (status bridge) --
235
+ # and for those only. Hook-less workspaces and the other modes resolve on flapping
236
+ # idle: markers instead (§5.5). ⚠️ A dsh worker running a profile that does not
237
+ # implement the status contract is the one case that LOOKS like claude but is not:
238
+ # it accepts the send and then burns both waits. One timeout on a dsh worker whose
239
+ # pane clearly finished means that profile, so switch that worker to markers.
200
240
  sendwait() {
201
241
  local sid="${1:?}" p="${2:?}" seq="${3:-$(date +%s)}" body r
242
+ # `wait:"stop,exit"`, never the `wait:true` default set: that set also carries
243
+ # `idle`, which is INFERRED from output stabilization and flaps mid-turn. On a
244
+ # dsh worker whose TUI repaints rarely the session reads `idle` while the model
245
+ # is still answering, and the re-wait below then resolved in 0 ms with
246
+ # `signal:"idle"` on a turn that had another three minutes to run (measured).
247
+ # A wait named after the end of a turn should only end with the turn, or with
248
+ # the worker. ⚠️ This is also what makes a wrong mode LOUD: the modes that
249
+ # cannot deliver `stop` answer 400 (before writing anything) instead of
250
+ # resolving on a flap, which is the answer that sends you to markers (§5.5).
202
251
  body=$(jq -nc --arg p "$p" --arg c "$CID-$sid" --argjson s "$seq" \
203
- '{input:($p+"\r"),useMux:true,clientId:$c,seq:$s,wait:true,waitTimeout:20000}')
252
+ '{input:($p+"\r"),useMux:true,clientId:$c,seq:$s,wait:"stop,exit",waitTimeout:20000}')
204
253
  r=$("${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" \
205
254
  -H 'Content-Type: application/json' --data-binary "$body")
206
255
  if jq -e '.data.delivered and .data.wait.timedOut' <<<"$r" >/dev/null 2>&1; then
207
256
  "${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" -H 'Content-Type: application/json' \
208
257
  -d "$(jq -nc --arg c "$CID-$sid" --argjson s "$(date +%s)" \
209
258
  '{input:"\r",useMux:true,clientId:$c,seq:$s}')" >/dev/null
259
+ # The resend is a tagged DUPLICATE, so the server skips the write and reports
260
+ # `delivered:false` for it -- truthfully, but about the wrong send. The first
261
+ # one delivered, so carry that forward, or §1's cleanup reads a completed turn
262
+ # as an undelivered one and keeps a finished worker forever.
210
263
  r=$("${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" \
211
- -H 'Content-Type: application/json' --data-binary "$(jq -c '.waitTimeout=580000' <<<"$body")")
264
+ -H 'Content-Type: application/json' --data-binary "$(jq -c '.waitTimeout=580000' <<<"$body")" \
265
+ | jq -c 'if .success and (.data.wait.ended | not) then .data.delivered = true else . end')
212
266
  fi
213
267
  printf '%s\n' "$r"
214
268
  }
215
- # last_text <sid> [prev] -> that worker's last assistant message. Polled, because the
216
- # transcript write LAGS the stop signal, and "some text exists" is not "THIS turn's
217
- # text exists": right after a SECOND turn on the same worker the endpoint still serves
269
+ # last_text <sid> [prev] -> that worker's last assistant message (claude, codex and
270
+ # deepseek write a real transcript; the other modes have none, so read the terminal
271
+ # instead -- §5.4). Polled, because the transcript write LAGS the stop signal, and
272
+ # "some text exists" is not "THIS turn's text exists": right after a SECOND turn on the same worker the endpoint still serves
218
273
  # the previous answer for a beat (observed live). When reading consecutive turns, pass
219
274
  # the previous answer as [prev]: the poll then holds out for text that differs from it,
220
275
  # falling back to whatever it last saw if the budget runs dry, so an honestly repeated
@@ -233,10 +288,10 @@ last_text() {
233
288
  # The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
234
289
  # bare on purpose: the write condition above anchors on it with $, so an inline comment
235
290
  # here would fail that match and rewrite this file on every single bootstrap.
236
- CODEMAN_PREAMBLE=1.19.0
291
+ CODEMAN_PREAMBLE=1.20.0
237
292
  PREAMBLE
238
293
  )
239
- . "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.19.0 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
294
+ . "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.20.0 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
240
295
  ```
241
296
 
242
297
  Every later Bash call that touches the API starts with the same two loader lines from
@@ -287,8 +342,9 @@ and no per-call body to hand-build.
287
342
 
288
343
  ```bash
289
344
  . "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader
290
- [ "${CODEMAN_PREAMBLE:-}" = 1.19.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
345
+ [ "${CODEMAN_PREAMBLE:-}" = 1.20.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
291
346
  N=(alpha beta) # INVENT one fresh case name per worker; never list cases first
347
+ # (a name may carry a mode: `beta:deepseek`, see below)
292
348
  T=('reply with one line: the absolute path of your working directory'
293
349
  'reply with one line: your model name') # tasks, same order as N
294
350
 
@@ -353,6 +409,37 @@ Four things this block leans on, each one link away, no detour needed to run it:
353
409
  - Each `sendwait` costs that worker one billed turn, as does every prompt you send it.
354
410
  - Deleting the sessions does **not** remove the case directories: §5.14.
355
411
 
412
+ ### DeepSeek Harness workers
413
+
414
+ The block above spawns claude workers. Any entry in `N` may instead name a mode
415
+ (`beta:deepseek`), and **a `deepseek` worker is driven by the same four verbs, with no
416
+ change to the rest of the block**: `spawn_workers` waits for its composer, `sendwait`
417
+ blocks on its real end-of-turn signal, `last_text` reads its answer, `delete_session`
418
+ removes it.
419
+
420
+ That is true of no other non-claude mode, and it is worth knowing why: the DeepSeek
421
+ Harness TUI reports `idle`/`working`/`blocked` to Codeman over the supervisor contract it
422
+ implements, so dsh is the one external CLI with definitive `stop`/`blocked` signals
423
+ instead of guessed-from-silence ones — and it writes a structured transcript, which is
424
+ what `last-response` reads for it. `shell`, `opencode`, `codex`, `gemini`, `antigravity`,
425
+ `pi` and `grok` have neither and still need markers ([§5.5](reference/verbs.md#55-markers-for-hook-less-workers)).
426
+
427
+ Three things to know before you spawn one:
428
+
429
+ - **It needs a pane-capable profile.** `dsh` ships only `web`/`headless`, so the terminal
430
+ agent is always an installed profile. `GET /api/v1/deepseek/status` answers both
431
+ questions separately (`available` = the binary, `runnable` = a profile that can drive a
432
+ pane); a spawn without one fails with `OPERATION_FAILED` rather than falling back.
433
+ - **Do not task it on the strength of a `stop` alone.** The harness reports `idle` at
434
+ boot ~300 ms *before* its composer paints (measured 2.26 s vs 2.56 s), so a `sendwait`
435
+ fired straight after `quick-start` resolves on that boot signal, reports a turn that
436
+ never ran, and leaves the prompt in a pane that was not yet taking input. Letting
437
+ `spawn_worker` gate on readiness is what steps past that edge; it is not optional.
438
+ - **A profile that does not implement the contract looks like a hang.** Codeman cannot
439
+ know at spawn time whether one does. The tell is a `sendwait` that times out on a
440
+ worker whose pane clearly finished: that profile is one of them, so drive it with
441
+ markers instead.
442
+
356
443
  ## 2. What do you want to do?
357
444
 
358
445
  One row per job. Acting on this table alone is correct; the §5 links are the detail.
@@ -360,10 +447,10 @@ One row per job. Acting on this table alone is correct; the §5 links are the de
360
447
  | I want to | Call | Detail |
361
448
  |-----------|------|--------|
362
449
  | start a worker **where the work is** | `POST /api/v1/quick-start {"caseName":…}`, which **creates** `~/codeman-cases/<name>` unless the name is already a case. Any other path (a git worktree): `POST /api/v1/sessions {"workingDir":…}` then `POST /api/v1/sessions/:id/interactive`. Both install hooks by default, so expect full signals in either, and **verify** rather than assume. N workers means N worktrees | [§5.1](reference/verbs.md#51-where-to-spawn) |
363
- | know a new worker can accept a prompt | `GET .../wait-output?match=shift+tab&from=buffer` (urlencode the `+`) | [§5.2](reference/verbs.md#52-readiness) |
364
- | deliver a task **and** know when it finished | `POST .../input` with `"input":"…\r"`, `clientId`, `seq`, `"wait":true`. Resolves on `stop`, so it is trustworthy only where the workspace **has hooks** (claude mode; installed by default, but the operator can disable it and remote sessions never get them). Costs the worker one billed turn | [§5.3](reference/verbs.md#53-send-a-task-and-wait) |
450
+ | know a new worker can accept a prompt | `GET .../wait-output?match=shift+tab&from=buffer` (urlencode the `+`); a `deepseek` worker draws `❯` instead, and its boot `stop` fires ~300 ms BEFORE that, so never read the signal as readiness | [§5.2](reference/verbs.md#52-readiness) |
451
+ | deliver a task **and** know when it finished | `POST .../input` with `"input":"…\r"`, `clientId`, `seq`, `"wait":true`. Resolves on `stop`, so it is trustworthy where the signal is real: claude mode with hooks (installed by default, but the operator can disable it and remote sessions never get them) and `deepseek` mode through its status bridge. Costs the worker one billed turn | [§5.3](reference/verbs.md#53-send-a-task-and-wait) |
365
452
  | know a hook-less worker finished | it has no `stop`, and `wait:true` there resolves on flapping `idle` **without erroring**: make it print a split, unique marker and `wait-output` on that instead | [§5.5](reference/verbs.md#55-markers-for-hook-less-workers) |
366
- | read the answer | `GET .../last-response`, **polled** (claude/codex only; empty for the other modes) | [§5.4](reference/verbs.md#54-read-the-answer) |
453
+ | read the answer | `GET .../last-response`, **polled** (claude, codex and deepseek write a transcript; empty for the other modes) | [§5.4](reference/verbs.md#54-read-the-answer) |
367
454
  | know if it is alive | `GET .../wait?until=exit&timeout=1000`: an immediate `signal:"exit"` means dead. `status` and `pid` both lie | [§5.6](reference/verbs.md#56-alive-and-stuck) |
368
455
  | know if it is stuck | `GET .../active-tools` and `GET .../run-summary` are structured and free; two `terminal?tail=` samples are the crude fallback | [§5.6](reference/verbs.md#56-alive-and-stuck) |
369
456
  | make a runaway worker stop | `POST .../input {"input":"\u001b"}` (ESC, **no** `\r`). Deleting the session would destroy the conversation instead | [§5.7](reference/verbs.md#57-interrupt-without-destroying) |
@@ -464,7 +551,7 @@ these**; open the one row you actually hit.
464
551
  | [5.1 Where to spawn](reference/verbs.md#51-where-to-spawn) | the work is **not** a fresh scratch case: a linked case, a git worktree, any path that already existed. Hooks are absent there, which silently breaks send-and-wait. The costliest mistake in this skill |
465
552
  | [5.2 Readiness](reference/verbs.md#52-readiness) | a worker never drew its composer, or you need the trust-dialog ladder by hand |
466
553
  | [5.3 Send a task and wait](reference/verbs.md#53-send-a-task-and-wait) | the `sendwait` body, its signals, and the duplicate-resend loop |
467
- | [5.4 Read the answer](reference/verbs.md#54-read-the-answer) | `last_text` came back empty, or the mode is not claude/codex |
554
+ | [5.4 Read the answer](reference/verbs.md#54-read-the-answer) | `last_text` came back empty, or the mode is not claude/codex/deepseek |
468
555
  | [5.5 Markers for hook-less workers](reference/verbs.md#55-markers-for-hook-less-workers) | the worker has no `stop` hook: synchronize on a split, unique printed marker |
469
556
  | [5.6 Alive and stuck](reference/verbs.md#56-alive-and-stuck) | is it dead or just slow? `status` and `pid` both lie |
470
557
  | [5.7 Interrupt without destroying](reference/verbs.md#57-interrupt-without-destroying) | a runaway worker you want to stop but keep |
@@ -1,4 +1,4 @@
1
- # ---- Codeman agent preamble 1.19.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
1
+ # ---- Codeman agent preamble 1.20.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
2
2
  API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
3
3
  SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
4
4
  # Credentials, cheapest first. Your session has usually INHERITED the server's
@@ -43,23 +43,53 @@ _composer_up() { # <sid> <timeoutMs> -> "true"/"false". `shift+tab` is the one
43
43
  --data-urlencode 'match=shift+tab' --data-urlencode 'from=buffer' \
44
44
  --data-urlencode "timeout=$2" | jq -r '.data.wait.matched // false'
45
45
  }
46
+ _dsh_up() { # <sid> <timeoutMs> -> "true"/"false". The DeepSeek Harness TUI's
47
+ # composer glyph. Override with DSH_READY_MARK for a profile that draws another one.
48
+ "${CURL[@]}" -G "$API/api/v1/sessions/$1/wait-output" \
49
+ --data-urlencode "match=${DSH_READY_MARK:-❯}" --data-urlencode 'from=buffer' \
50
+ --data-urlencode "timeout=$2" | jq -r '.data.wait.matched // false'
51
+ }
46
52
  # spawn_worker <caseName> [mode] -> session id on stdout, diagnostics on stderr.
47
53
  # 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.
54
+ # a READY worker whose end-of-turn signal can be trusted -- a claude worker in a
55
+ # hook-carrying case, or a `deepseek` worker whose harness TUI drew its composer.
56
+ # Anything less is rc 1 with EMPTY stdout, and the half-spawned session is deleted here
57
+ # rather than handed back, because a worker that never drew its composer would eat the
58
+ # task prompt with its trust dialog. There is deliberately no pid poll: wait-output
59
+ # already blocks until the composer draws, and pid!=null proved startup, never readiness.
53
60
  spawn_worker() {
54
61
  local name="${1:?spawn_worker needs a case name}" mode="${2:-claude}" q sid cp r
55
62
  # parentSessionId doubles the CURL header, so a spawn_worker copied off the shared
56
63
  # curl (or a body someone rebuilt from this recipe) still carries its lineage.
64
+ # deepseek: ask for the same permission posture the Run button sends, because the
65
+ # harness's own default (`workspace-write`) still ASKS, and a worker that stops on
66
+ # an approval row is a worker no fan-out can finish. It is not an escalation --
67
+ # claude workers already spawn with permissions skipped, and in multi-user mode the
68
+ # server clamps this back to `workspace-write` for an owner without the grant.
69
+ # Spawn by hand (§5.1) when you want a worker that asks.
57
70
  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}')")
71
+ -d "$(jq -nc --arg n "$name" --arg m "$mode" --arg p "$SELF" \
72
+ '{caseName:$n,mode:$m,parentSessionId:$p}
73
+ + (if $m == "deepseek" then {deepSeekConfig:{permissionMode:"danger-full-access"}} else {} end)')")
59
74
  sid=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$q")
60
75
  # NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
61
76
  [ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
62
- [ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # only claude draws a composer
77
+ if [ "$mode" = deepseek ]; then
78
+ # The one non-claude mode with REAL end-of-turn signals: its TUI reports
79
+ # idle/working/blocked to Codeman, so sendwait, until=stop and the Approvals
80
+ # Inbox all work here exactly as they do for claude. No hook file to vet
81
+ # (the bridge is env-injected, not a workspace file) and no trust dialog.
82
+ # ⚠️ Readiness is still not optional, and NOT interchangeable with the stop
83
+ # signal: the harness's boot report lands ~300ms BEFORE the composer paints
84
+ # (measured 2.26s vs 2.56s after spawn), so a sendwait fired straight after
85
+ # quick-start returns on that BOOT signal, reports a turn that never ran, and
86
+ # strands the prompt in a pane that was not yet taking input.
87
+ r=$(_dsh_up "$sid" 45000)
88
+ [ "$r" = true ] || { echo "dsh worker $sid never drew a composer: no pane-capable profile, a profile whose composer is not '${DSH_READY_MARK:-❯}' (set DSH_READY_MARK), or a harness that failed to boot -- check GET /api/v1/deepseek/status. Deleted it" >&2
89
+ delete_session "$sid" >/dev/null; return 1; }
90
+ printf '%s\n' "$sid"; return 0
91
+ fi
92
+ [ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # no other mode draws a composer to wait on
63
93
  # The server installs hooks into every claude workspace now, so this grep normally
64
94
  # passes; it stays because the install is gated on a setting the operator can turn
65
95
  # off, remote sessions never get hooks, and a session created by an older server
@@ -88,19 +118,25 @@ spawn_worker() {
88
118
  delete_session "$sid" >/dev/null; return 1; }
89
119
  printf '%s\n' "$sid"
90
120
  }
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.
121
+ # spawn_workers <caseName[:mode]>... -> one "<caseName> <sessionId>" line per worker, in
122
+ # order; the sessionId column is EMPTY for a spawn that failed (stderr has why).
123
+ # CONCURRENT: N workers cost about what one costs. Spawning them one Bash call at a time
124
+ # is the single biggest avoidable delay in this skill. A bare name is a claude worker;
125
+ # `beta:deepseek` makes that one a DeepSeek Harness worker, and a mixed fleet is one
126
+ # call. Case names must be UNIQUE: two workers in one case directory co-edit the same
127
+ # tree (§4), so a repeat is an error here, not a race (the mode never disambiguates two
128
+ # workers, since they would still share the directory).
96
129
  spawn_workers() {
97
- local d n i=0
130
+ local d spec n m i=0
98
131
  [ "$#" -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; }
132
+ [ -z "$(printf '%s\n' "$@" | sed 's/:.*//' | sort | uniq -d)" ] || { echo "spawn_workers: duplicate case names" >&2; return 1; }
100
133
  d=$(mktemp -d "${TMPDIR:-/tmp}/codeman-spawn.XXXXXX") || return 1
101
- for n in "$@"; do ( spawn_worker "$n" > "$d/$i" ) & i=$((i+1)); done
134
+ for spec in "$@"; do
135
+ n=${spec%%:*}; m=${spec#*:}; [ "$m" = "$spec" ] && m=claude
136
+ ( spawn_worker "$n" "$m" > "$d/$i" ) & i=$((i+1))
137
+ done
102
138
  wait
103
- i=0; for n in "$@"; do printf '%s %s\n' "$n" "$(cat "$d/$i" 2>/dev/null)"; i=$((i+1)); done
139
+ i=0; for spec in "$@"; do printf '%s %s\n' "${spec%%:*}" "$(cat "$d/$i" 2>/dev/null)"; i=$((i+1)); done
104
140
  rm -rf "$d"
105
141
  }
106
142
  # sendwait <sid> <prompt> [seq] -> blocks until that worker's turn ENDS (~10 min ceiling
@@ -116,27 +152,46 @@ spawn_workers() {
116
152
  # (observed live). So the first wait is short; on its timeout a bare \r goes out (the
117
153
  # missing Enter when the prompt is stranded, a no-op when the turn is genuinely
118
154
  # 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).
155
+ # tagged duplicate: it re-waits without retyping (§5.3). Trustworthy for a worker
156
+ # spawn_worker handed back -- claude (hooks vetted) or deepseek (status bridge) --
157
+ # and for those only. Hook-less workspaces and the other modes resolve on flapping
158
+ # idle: markers instead (§5.5). ⚠️ A dsh worker running a profile that does not
159
+ # implement the status contract is the one case that LOOKS like claude but is not:
160
+ # it accepts the send and then burns both waits. One timeout on a dsh worker whose
161
+ # pane clearly finished means that profile, so switch that worker to markers.
122
162
  sendwait() {
123
163
  local sid="${1:?}" p="${2:?}" seq="${3:-$(date +%s)}" body r
164
+ # `wait:"stop,exit"`, never the `wait:true` default set: that set also carries
165
+ # `idle`, which is INFERRED from output stabilization and flaps mid-turn. On a
166
+ # dsh worker whose TUI repaints rarely the session reads `idle` while the model
167
+ # is still answering, and the re-wait below then resolved in 0 ms with
168
+ # `signal:"idle"` on a turn that had another three minutes to run (measured).
169
+ # A wait named after the end of a turn should only end with the turn, or with
170
+ # the worker. ⚠️ This is also what makes a wrong mode LOUD: the modes that
171
+ # cannot deliver `stop` answer 400 (before writing anything) instead of
172
+ # resolving on a flap, which is the answer that sends you to markers (§5.5).
124
173
  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}')
174
+ '{input:($p+"\r"),useMux:true,clientId:$c,seq:$s,wait:"stop,exit",waitTimeout:20000}')
126
175
  r=$("${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" \
127
176
  -H 'Content-Type: application/json' --data-binary "$body")
128
177
  if jq -e '.data.delivered and .data.wait.timedOut' <<<"$r" >/dev/null 2>&1; then
129
178
  "${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" -H 'Content-Type: application/json' \
130
179
  -d "$(jq -nc --arg c "$CID-$sid" --argjson s "$(date +%s)" \
131
180
  '{input:"\r",useMux:true,clientId:$c,seq:$s}')" >/dev/null
181
+ # The resend is a tagged DUPLICATE, so the server skips the write and reports
182
+ # `delivered:false` for it -- truthfully, but about the wrong send. The first
183
+ # one delivered, so carry that forward, or §1's cleanup reads a completed turn
184
+ # as an undelivered one and keeps a finished worker forever.
132
185
  r=$("${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" \
133
- -H 'Content-Type: application/json' --data-binary "$(jq -c '.waitTimeout=580000' <<<"$body")")
186
+ -H 'Content-Type: application/json' --data-binary "$(jq -c '.waitTimeout=580000' <<<"$body")" \
187
+ | jq -c 'if .success and (.data.wait.ended | not) then .data.delivered = true else . end')
134
188
  fi
135
189
  printf '%s\n' "$r"
136
190
  }
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
191
+ # last_text <sid> [prev] -> that worker's last assistant message (claude, codex and
192
+ # deepseek write a real transcript; the other modes have none, so read the terminal
193
+ # instead -- §5.4). Polled, because the transcript write LAGS the stop signal, and
194
+ # "some text exists" is not "THIS turn's text exists": right after a SECOND turn on the same worker the endpoint still serves
140
195
  # the previous answer for a beat (observed live). When reading consecutive turns, pass
141
196
  # the previous answer as [prev]: the poll then holds out for text that differs from it,
142
197
  # falling back to whatever it last saw if the budget runs dry, so an honestly repeated
@@ -155,4 +210,4 @@ last_text() {
155
210
  # The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
156
211
  # bare on purpose: the write condition above anchors on it with $, so an inline comment
157
212
  # here would fail that match and rewrite this file on every single bootstrap.
158
- CODEMAN_PREAMBLE=1.19.0
213
+ CODEMAN_PREAMBLE=1.20.0
@@ -237,7 +237,10 @@ minutes, never retry the credential.
237
237
  flushed slightly *after* the `stop` hook fires, so a read taken the instant the wait
238
238
  returns is too early (verified live: empty on the first call, full prose seconds later).
239
239
  It is also `""` before the worker's first completed turn, and permanently `""` for
240
- `shell`, `opencode`, `gemini`, `antigravity`, `pi` and `grok`, which write no Claude transcript.
240
+ `shell`, `opencode`, `gemini`, `antigravity`, `pi` and `grok`, which write no transcript at
241
+ all. `deepseek` is NOT one of those — it is read from `$DSH_HOME/sessions/**` and lags
242
+ for the same reason claude does (the harness finalizes the assistant message just after
243
+ it reports `idle`), so poll it the same way.
241
244
 
242
245
  **Fix** Poll it, bounded (10 tries, 1 s apart). If it is still empty on a hook-less mode,
243
246
  that is expected, not a failure: read `terminal?tail=` and strip ANSI instead.
@@ -279,7 +282,7 @@ than into an existing checkout.
279
282
  | start case + session in one call | `POST /api/v1/quick-start` |
280
283
  | create a session in an arbitrary directory (no case, **no PTY**, id at `.data.session.id`) | `POST /api/v1/sessions`, then `POST /api/v1/sessions/:id/interactive` or `.../shell` to start it, see [Starting a worker](#starting-a-worker) |
281
284
  | send input | `POST /api/v1/sessions/:id/input` |
282
- | **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**, see [symptom 7](#7-last-response-returns-an-empty-string-right-after-stop) |
285
+ | **read a worker's answer** (claude/codex/deepseek) | `GET /api/v1/sessions/:id/last-response` → `.data.{text,timestamp}`, clean transcript text, no TUI noise. ⚠️ **Poll it**, see [symptom 7](#7-last-response-returns-an-empty-string-right-after-stop) |
283
286
  | 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 |
284
287
  | full tmux scrollback (context bomb; post-mortems only) | `GET /api/v1/sessions/:id/terminal?full=1` |
285
288
  | background agents, one session | `GET /api/v1/sessions/:id/subagents` |
@@ -321,7 +324,7 @@ on signals and markers for exactly this reason.
321
324
  ⚠️ `GET /api/v1/sessions/:id/output` → `.data.textOutput` looks like the obvious read
322
325
  but stays **empty for interactive tmux-backed sessions** (it is fed only by the legacy
323
326
  JSON-stream path). Verified empty on live claude and shell sessions. Use
324
- `last-response` for claude/codex answers; only fall back to `terminal?tail=` for
327
+ `last-response` for claude/codex/deepseek answers; only fall back to `terminal?tail=` for
325
328
  hook-less modes, or to diagnose a prompt that was never submitted, and strip ANSI:
326
329
 
327
330
  ```bash
@@ -336,14 +339,14 @@ ESC=$(printf '\033')
336
339
 
337
340
  `POST /api/v1/quick-start` body (all optional):
338
341
  `{"caseName":"worker-1","mode":"claude","sessionName":"w9-worker","effort":"high"}`
339
- , `mode` ∈ `claude|shell|opencode|codex|gemini|antigravity|pi|grok`; response is
342
+ , `mode` ∈ `claude|shell|opencode|codex|gemini|antigravity|pi|grok|deepseek`; response is
340
343
  `.data.{sessionId, caseName, casePath}`. Creates the case directory (a real directory
341
344
  on the user's disk) if missing, do not retry it in a loop, and remember the name.
342
345
 
343
346
  ⚠️ A `mode` whose CLI is **not installed on the server** fails the spawn with
344
347
  `OPERATION_FAILED`; it never falls back to claude. Probe first whenever you did not pick
345
348
  the mode yourself: `GET /api/v1/claude/status`, `GET /api/v1/opencode/status`,
346
- `GET /api/v1/codex/status`, `GET /api/v1/gemini/status`, `GET /api/v1/antigravity/status`, `GET /api/v1/grok/status`
349
+ `GET /api/v1/codex/status`, `GET /api/v1/gemini/status`, `GET /api/v1/antigravity/status`, `GET /api/v1/grok/status`, `GET /api/v1/deepseek/status`
347
350
  and `GET /api/v1/pi/status` each return `.data.{available, path}` (no session needed).
348
351
  Pi's and grok's also carry `.data.version`, because `pi` is a short generic name and
349
352
  `grok` is a name with npm squatters, so an unrelated binary on `$PATH` can shadow either:
@@ -464,7 +467,7 @@ Quirks that will bite you:
464
467
  - ⚠️ **`active-tools` proves presence, never absence.** It is fed by the BashToolParser,
465
468
  which reads Claude's rendered `● Bash(…)` lines, and `_processExpensiveParsers`
466
469
  returns early for every external CLI mode (`session.ts:2261`), so it is permanently
467
- `[]` on `opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`. ⚠️ **`shell` is NOT one of those**
470
+ `[]` on `opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`. ⚠️ **`shell` is NOT one of those**
468
471
  (`isExternalCliMode`, `session.ts:174-183`, lists only those six), so the parser does
469
472
  run on a shell worker, and `TEXT_COMMAND_PATTERN` (`bash-tool-parser.ts:89`) matches
470
473
  bare `tail|cat|head|less|grep|watch|multitail <path>` lines with no `● Bash(` wrapper:
@@ -640,10 +643,14 @@ block, so a linked case or a raw `workingDir` had no hooks at all. `POST
640
643
  session-create path installs hooks regardless of how the directory got there. See
641
644
  [symptom 8](#8-send-and-wait-resolves-instantly-with-signalidle-and-the-answer-is-last-turns).
642
645
 
643
- Default `until` set: `stop,idle,exit`. On non-claude modes the server silently drops
644
- `stop`/`blocked` from the *default* set (echoed back as `wait.until`, e.g.
646
+ Default `until` set: `stop,idle,exit`. On modes with no hook signals the server silently
647
+ drops `stop`/`blocked` from the *default* set (echoed back as `wait.until`, e.g.
645
648
  `["idle","exit"]` on shell); requesting them *explicitly* there is a 400 naming the
646
- mode. ⚠️ That 400 is about **mode**, so a hooks-less *claude* session accepts
649
+ mode. ⚠️ `deepseek` is not one of those: its harness reports its own lifecycle, so it
650
+ keeps the full default set and accepts an explicit `until=stop`. ⚠️ For dsh the answer is
651
+ per-SESSION rather than per-mode — a session created with `statusReporting: false` has no
652
+ bridge, and an explicit `until=stop` there is a 400 naming that setting. ⚠️ That 400 is
653
+ otherwise about **mode**, so a hooks-less *claude* session accepts
647
654
  `until=stop` happily and then never resolves it. ⚠️ On hook-less modes the lifecycle
648
655
  signals are also **coarse in practice**: a
649
656
  short shell command produced **no** `idle` transition within 60 s (verified live), so
@@ -791,7 +798,8 @@ for environment and setup problems.
791
798
  | `CODEMAN_MUX` unset but you seem to be in a session | remote-SSH case: the env vars are not exported there. Fail closed, refuse to act |
792
799
  | connection refused from inside a container | a loopback-bound server is unreachable from a container, and `CODEMAN_DOCKER_BRIDGE_HOOKS=1` does **not** fix that: it opens a hooks-only listener, so hook events start flowing but `/api/v1/*` stays refused. Driving the API from inside a Docker case needs a reachable bind (an operator decision); report it, don't retry |
793
800
  | wait routes 404 on a valid session id | read the `.error` text: a `Route ...` prefix means the server predates the wait endpoints (< 1.13.0; a dev build can serve them while reporting an older version, so probe, never version-compare), poll `terminal?tail=` and say so. `Session ... not found` means your id is wrong, not the server |
794
- | wait on `stop` never resolves | non-claude mode, or hooks not reaching the server (Docker/remote), or a case created by Codeman < 1.13.0 against an `--https` install (its hook curls lacked `-k` and TLS-failed silently; a 1.13.0+ server rewrites them the next time a session starts in that case). Use markers or `idle,exit` |
801
+ | wait on `stop` never resolves | a mode with no hook signals, or hooks not reaching the server (Docker/remote), or a case created by Codeman < 1.13.0 against an `--https` install (its hook curls lacked `-k` and TLS-failed silently; a 1.13.0+ server rewrites them the next time a session starts in that case). Use markers or `idle,exit` |
802
+ | wait on `stop` never resolves, on a **dsh** worker whose pane clearly finished | that profile does not implement the harness's supervisor contract, which Codeman cannot detect at request time (an unrecognized profile is treated as launchable on purpose). The wait is accepted and then times out. Drive that worker with markers, or switch to a profile that reports — `@deepseek-harness-tui/dsh-tui` does |
795
803
  | new claude worker ignores its first prompt | it was showing the first-run trust dialog and Codeman's auto-accept did not fire (it is bounded by a 90 s window and an attempt cap); use the readiness recipe in SKILL.md, wait for `shift+tab` first, accept the dialog only as the bounded fallback |
796
804
  | readiness burns its whole budget, then the worker answers fine anyway | you matched `bypass`, which is the statusline of ONE permission mode. Codeman spawns `--dangerously-skip-permissions` by default, but the server's `claudeMode` setting also has `auto` (`auto mode on`), `allowedTools` and `normal` (both `don't ask on`), and the effective per-session value is not exposed on `GET /api/v1/sessions/:id`. Match **`shift+tab`** instead: every mode's status bar ends `(shift+tab to cycle)` (measured per mode against claude-cli 2.1.226). Expect `blocked` signals mid-turn on the non-default modes |
797
805
  | ANSI escapes survive the strip pipeline | `sed -e 's/\x1b…'` on macOS: `\x1b` is GNU-only, BSD sed matches nothing and strips nothing. Use the `ESC=$(printf '\033')` form above |
@@ -56,7 +56,7 @@ own head: the worker enforcing the cap is the one who has to be told about it.
56
56
  | synchronize on end of turn | HTTP `wait until=stop` (fires for message-initiated turns too, verified live) |
57
57
  | liveness / death check | HTTP `wait?until=exit` |
58
58
  | interrupt a running turn (break-glass) | HTTP input, a bare `\x1b` with no `\r` |
59
- | non-claude modes (`shell`/`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`) | HTTP only (no other CLI has messaging) |
59
+ | non-claude modes (`shell`/`opencode`/`codex`/`gemini`/`antigravity`/`pi`/`grok`/`deepseek`) | HTTP only (no other CLI has messaging) |
60
60
  | delete | HTTP, via SKILL.md's `delete_session` guard |
61
61
 
62
62
  ## Availability: probe, never assume
@@ -347,7 +347,7 @@ Without a break-glass, a pair with a bad brief is a token bonfire with no off sw
347
347
 
348
348
  ### Mixed fleets: the pairing matrix
349
349
 
350
- Non-claude workers (`shell`, `opencode`, `codex`, `gemini`, `antigravity`, `pi`, `grok`) cannot be peers
350
+ Non-claude workers (`shell`, `opencode`, `codex`, `gemini`, `antigravity`, `pi`, `grok`, `deepseek`) cannot be peers
351
351
  at all; no other CLI has this feature. Their tasks route over HTTP, and you never mention
352
352
  messaging in their briefs. The claude half of the fleet can use messaging among itself,
353
353
  subject to the namespace rule: **messaging works between two sessions that share one
@@ -198,6 +198,59 @@ delete_session "$SID"
198
198
  Increment `SEQ` for every *new* input to the same worker. Reuse the same `SEQ` only to
199
199
  re-ask about the same delivery (the duplicate-wait loop above).
200
200
 
201
+ ## Flow 1b: DeepSeek Harness worker, end to end
202
+
203
+ A `deepseek` worker is driven with the same four verbs as a claude one, because the
204
+ harness reports its own lifecycle: its `stop` is a real end-of-turn signal, and its
205
+ answer comes from a real transcript. The differences are all at the edges.
206
+
207
+ ```bash
208
+ # 0. Is there anything to spawn? `available` is the binary, `runnable` is a profile
209
+ # that can drive a pane -- dsh ships only web/headless, so the two differ.
210
+ "${CURL[@]}" "$API/api/v1/deepseek/status" | jq -c '{available:.data.available,runnable:.data.runnable,profile:.data.defaultProfile}'
211
+
212
+ # 1. Spawn. `deepSeekConfig` is optional: an absent profile picks the first
213
+ # pane-capable one, and an absent permissionMode leaves the harness on its own
214
+ # workspace-write default, which still ASKS before it acts.
215
+ Q=$("${CURL[@]}" -X POST "$API/api/v1/quick-start" -H 'Content-Type: application/json' \
216
+ -d '{"caseName":"dsh-worker","mode":"deepseek","deepSeekConfig":{"permissionMode":"danger-full-access"}}')
217
+ SID=$(jq -r 'if .success then .data.sessionId else empty end' <<<"$Q")
218
+ [ -n "$SID" ] || { jq -c '{error, errorCode}' <<<"$Q"; exit 1; } # OPERATION_FAILED = no runnable profile
219
+ CREATED+=("$SID")
220
+
221
+ # 2. Readiness, and ONLY readiness. ⚠️ Do not use the stop signal for this: the
222
+ # harness reports idle at BOOT, ~300 ms before the composer paints.
223
+ "${CURL[@]}" -G "$API/api/v1/sessions/$SID/wait-output" \
224
+ --data-urlencode 'match=❯' --data-urlencode 'from=buffer' --data-urlencode 'timeout=45000' \
225
+ | jq -e '.data.wait.matched' >/dev/null || { echo "no composer"; delete_session "$SID"; exit 1; }
226
+
227
+ # 3. Task it. Identical to a claude worker, including the \r and the (clientId, seq).
228
+ "${CURL[@]}" -X POST "$API/api/v1/sessions/$SID/input" -H 'Content-Type: application/json' \
229
+ -d '{"input":"Read calc.py and tell me in one sentence whether add() is correct.\r","useMux":true,"clientId":"codeman-dsh-1","seq":1,"wait":"stop,exit","waitTimeout":300000}' \
230
+ | jq -c '{delivered:.data.delivered,signal:.data.wait.signal,timedOut:.data.wait.timedOut}'
231
+
232
+ # 4. Read it. From $DSH_HOME/sessions/**, not the pane -- scraping a dsh pane returns
233
+ # its ASCII-art splash. Poll: the harness finalizes the message just after it
234
+ # reports idle. Two answers are not the model's words and say so:
235
+ # "Turn error: …" (the provider or harness failed) and "Turn ended: …" (early stop).
236
+ for _ in $(seq 1 15); do
237
+ TXT=$("${CURL[@]}" "$API/api/v1/sessions/$SID/last-response" | jq -r '.data.text')
238
+ [ -n "$TXT" ] && break; sleep 1
239
+ done
240
+ printf '%s\n' "$TXT"
241
+
242
+ # 5. Full conversation, if you need the tool calls too:
243
+ # "${CURL[@]}" "$API/api/v1/sessions/$SID/last-response?context=full" | jq -r '.data.messages[]|"[\(.label)] \(.text)"'
244
+
245
+ delete_session "$SID"
246
+ ```
247
+
248
+ ⚠️ **`wait:"stop,exit"`, not `wait:true`.** The default set also carries `idle`, which
249
+ for an external CLI is inferred from output stabilization: a dsh TUI that repaints
250
+ rarely reads as idle mid-turn, and a wait carrying `idle` then resolves in 0 ms on a
251
+ turn with minutes left to run (measured). The same reason the preamble's `sendwait`
252
+ asks for `stop,exit` on every mode.
253
+
201
254
  ## Flow 2: shell worker, marker-synchronized
202
255
 
203
256
  `shell` sessions have no hooks (`stop`/`blocked` are a 400 there), and their lifecycle