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.
- package/README.md +1 -1
- package/dist/config/dependency-registry.d.ts.map +1 -1
- package/dist/config/dependency-registry.js +27 -0
- package/dist/config/dependency-registry.js.map +1 -1
- package/dist/cron/cron-service.d.ts +3 -2
- package/dist/cron/cron-service.d.ts.map +1 -1
- package/dist/cron/cron-service.js +17 -3
- package/dist/cron/cron-service.js.map +1 -1
- package/dist/deepseek-status-shim.d.ts +77 -0
- package/dist/deepseek-status-shim.d.ts.map +1 -0
- package/dist/deepseek-status-shim.js +268 -0
- package/dist/deepseek-status-shim.js.map +1 -0
- package/dist/deepseek-transcript.d.ts +188 -0
- package/dist/deepseek-transcript.d.ts.map +1 -0
- package/dist/deepseek-transcript.js +633 -0
- package/dist/deepseek-transcript.js.map +1 -0
- package/dist/deepseek-web-server.d.ts +60 -0
- package/dist/deepseek-web-server.d.ts.map +1 -0
- package/dist/deepseek-web-server.js +249 -0
- package/dist/deepseek-web-server.js.map +1 -0
- package/dist/docker-hosts.d.ts.map +1 -1
- package/dist/docker-hosts.js +12 -0
- package/dist/docker-hosts.js.map +1 -1
- package/dist/mux-interface.d.ts +3 -1
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +4 -0
- package/dist/remote-hosts.js.map +1 -1
- package/dist/session.d.ts +27 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +47 -2
- package/dist/session.js.map +1 -1
- package/dist/tmux-manager.d.ts +23 -1
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +134 -5
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tui/tui-app.d.ts.map +1 -1
- package/dist/tui/tui-app.js +1 -0
- package/dist/tui/tui-app.js.map +1 -1
- package/dist/tui/tui-client.d.ts +1 -1
- package/dist/tui/tui-client.d.ts.map +1 -1
- package/dist/types/api.d.ts +1 -1
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/api.js.map +1 -1
- package/dist/types/session.d.ts +61 -4
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js +2 -1
- package/dist/types/session.js.map +1 -1
- package/dist/types/webview.d.ts +15 -0
- package/dist/types/webview.d.ts.map +1 -1
- package/dist/utils/deepseek-cli-resolver.d.ts +144 -0
- package/dist/utils/deepseek-cli-resolver.d.ts.map +1 -0
- package/dist/utils/deepseek-cli-resolver.js +339 -0
- package/dist/utils/deepseek-cli-resolver.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.c5cf1b8a.js +36 -0
- package/dist/web/public/app.c5cf1b8a.js.br +0 -0
- package/dist/web/public/app.c5cf1b8a.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.189c7bff.js → constants.f4ea0a19.js} +8 -1
- package/dist/web/public/constants.f4ea0a19.js.br +0 -0
- package/dist/web/public/constants.f4ea0a19.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/home-sessions.js +1 -0
- package/dist/web/public/home-sessions.js.br +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/i18n.9ae15b51.js +1 -0
- package/dist/web/public/i18n.9ae15b51.js.br +0 -0
- package/dist/web/public/i18n.9ae15b51.js.gz +0 -0
- package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
- package/dist/web/public/index.html +47 -14
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.87f14251.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.0454a3e9.js.gz +0 -0
- package/dist/web/public/mobile-handlers.0f83a608.js.gz +0 -0
- package/dist/web/public/mobile-overview.js +6 -0
- package/dist/web/public/mobile-overview.js.br +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.cab92e3f.css +1 -0
- package/dist/web/public/mobile.cab92e3f.css.br +0 -0
- package/dist/web/public/mobile.cab92e3f.css.gz +0 -0
- package/dist/web/public/notification-manager.5d229063.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.7c667123.js → panels-ui.344de87d.js} +41 -33
- package/dist/web/public/panels-ui.344de87d.js.br +0 -0
- package/dist/web/public/panels-ui.344de87d.js.gz +0 -0
- package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
- package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.ff0dae4c.js.gz +0 -0
- package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/session-ui.103656d3.js +68 -0
- package/dist/web/public/session-ui.103656d3.js.br +0 -0
- package/dist/web/public/session-ui.103656d3.js.gz +0 -0
- package/dist/web/public/settings-ui.3261b896.js +67 -0
- package/dist/web/public/settings-ui.3261b896.js.br +0 -0
- package/dist/web/public/settings-ui.3261b896.js.gz +0 -0
- package/dist/web/public/styles.1938fd02.css +1 -0
- package/dist/web/public/styles.1938fd02.css.br +0 -0
- package/dist/web/public/styles.1938fd02.css.gz +0 -0
- package/dist/web/public/subagent-windows.e6ca799f.js.gz +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.42c24949.js +1 -0
- package/dist/web/public/tab-rail-resize.42c24949.js.br +0 -0
- package/dist/web/public/tab-rail-resize.42c24949.js.gz +0 -0
- package/dist/web/public/terminal-ui.5e0c230c.js +2 -0
- package/dist/web/public/terminal-ui.5e0c230c.js.br +0 -0
- package/dist/web/public/terminal-ui.5e0c230c.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/upload.html.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.bd6882b8.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.6fee72f2.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.4ec670b0.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js +4 -1
- package/dist/web/public/webview-tabs.js.br +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/response-viewer-transcript.js +1 -1
- package/dist/web/response-viewer-transcript.js.map +1 -1
- package/dist/web/routes/approval-routes.d.ts.map +1 -1
- package/dist/web/routes/approval-routes.js +12 -2
- package/dist/web/routes/approval-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +51 -7
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/readmymind-routes.d.ts.map +1 -1
- package/dist/web/routes/readmymind-routes.js +5 -2
- package/dist/web/routes/readmymind-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +30 -2
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +158 -16
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +203 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/routes/webview-routes.d.ts.map +1 -1
- package/dist/web/routes/webview-routes.js +1 -0
- package/dist/web/routes/webview-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +68 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +107 -4
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +21 -3
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +63 -7
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +67 -9
- package/dist/web/session-wait-registry.js.map +1 -1
- package/dist/web/sse-events.d.ts +11 -2
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +11 -2
- package/dist/web/sse-events.js.map +1 -1
- package/package.json +2 -1
- package/skills/codeman/SKILL.md +121 -34
- package/skills/codeman/preamble.sh +81 -26
- package/skills/codeman/reference/endpoints.md +18 -10
- package/skills/codeman/reference/messaging.md +2 -2
- package/skills/codeman/reference/recipes.md +53 -0
- package/skills/codeman/reference/verbs.md +39 -8
- package/dist/web/public/app.84bf8955.js +0 -36
- package/dist/web/public/app.84bf8955.js.br +0 -0
- package/dist/web/public/app.84bf8955.js.gz +0 -0
- package/dist/web/public/constants.189c7bff.js.br +0 -0
- package/dist/web/public/constants.189c7bff.js.gz +0 -0
- package/dist/web/public/i18n.34016b89.js +0 -1
- package/dist/web/public/i18n.34016b89.js.br +0 -0
- package/dist/web/public/i18n.34016b89.js.gz +0 -0
- package/dist/web/public/mobile.983f064e.css +0 -1
- package/dist/web/public/mobile.983f064e.css.br +0 -0
- package/dist/web/public/mobile.983f064e.css.gz +0 -0
- package/dist/web/public/panels-ui.7c667123.js.br +0 -0
- package/dist/web/public/panels-ui.7c667123.js.gz +0 -0
- package/dist/web/public/session-ui.acd79900.js +0 -68
- package/dist/web/public/session-ui.acd79900.js.br +0 -0
- package/dist/web/public/session-ui.acd79900.js.gz +0 -0
- package/dist/web/public/settings-ui.115a2d1e.js +0 -67
- package/dist/web/public/settings-ui.115a2d1e.js.br +0 -0
- package/dist/web/public/settings-ui.115a2d1e.js.gz +0 -0
- package/dist/web/public/styles.64369ebd.css +0 -1
- package/dist/web/public/styles.64369ebd.css.br +0 -0
- package/dist/web/public/styles.64369ebd.css.gz +0 -0
- package/dist/web/public/tab-rail-resize.f3465ce1.js +0 -1
- package/dist/web/public/tab-rail-resize.f3465ce1.js.br +0 -0
- package/dist/web/public/tab-rail-resize.f3465ce1.js.gz +0 -0
- package/dist/web/public/terminal-ui.ef68a666.js +0 -2
- package/dist/web/public/terminal-ui.ef68a666.js.br +0 -0
- package/dist/web/public/terminal-ui.ef68a666.js.gz +0 -0
package/skills/codeman/SKILL.md
CHANGED
|
@@ -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.
|
|
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.
|
|
79
|
-
# ---- Codeman agent preamble 1.
|
|
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
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
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"
|
|
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" =
|
|
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
|
|
170
|
-
# the sessionId column is EMPTY for a spawn that failed (stderr has why).
|
|
171
|
-
# N workers cost about what one costs. Spawning them one Bash call at a time
|
|
172
|
-
# single biggest avoidable delay in this skill.
|
|
173
|
-
#
|
|
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
|
|
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
|
|
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
|
|
198
|
-
#
|
|
199
|
-
#
|
|
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:
|
|
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
|
|
216
|
-
#
|
|
217
|
-
#
|
|
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.
|
|
291
|
+
CODEMAN_PREAMBLE=1.20.0
|
|
237
292
|
PREAMBLE
|
|
238
293
|
)
|
|
239
|
-
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
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"
|
|
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" =
|
|
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
|
|
92
|
-
# the sessionId column is EMPTY for a spawn that failed (stderr has why).
|
|
93
|
-
# N workers cost about what one costs. Spawning them one Bash call at a time
|
|
94
|
-
# single biggest avoidable delay in this skill.
|
|
95
|
-
#
|
|
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
|
|
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
|
|
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
|
|
120
|
-
#
|
|
121
|
-
#
|
|
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:
|
|
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
|
|
138
|
-
#
|
|
139
|
-
#
|
|
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.
|
|
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
|
|
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
|
|
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. ⚠️
|
|
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 |
|
|
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
|