# ---- Codeman agent preamble 1.51.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ---- # Positional parameters are always written with braces, ${1} and never a bare dollar-digit: Claude Code # replaces a bare one in a skill body with the matching word of the skill's arguments, and §0 is pasted # from that body (test/agent-skill.test.ts pins it). API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}" SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}" # Credentials, cheapest first. Your session has usually INHERITED the server's # CODEMAN_PASSWORD already (§6 explains why, and what to do when it has not); # the data dir's .env is the documented fallback, the same one `codeman attach` # reads. The data dir is wherever the hook-secret file lives. Values may be # quoted or `export`-prefixed. ENV_FILE="${CODEMAN_HOOK_SECRET_FILE:+${CODEMAN_HOOK_SECRET_FILE%hook-secret}.env}" envval() { sed -n "s/^\(export \)\{0,1\}${1}=//p" "$ENV_FILE" | tail -1 | sed 's/^"\(.*\)"$/\1/; s/^'\''\(.*\)'\''$/\1/'; } if [ -z "${CODEMAN_PASSWORD:-}" ] && [ -n "$ENV_FILE" ] && [ -f "$ENV_FILE" ]; then CODEMAN_USERNAME=$(envval CODEMAN_USERNAME) CODEMAN_PASSWORD=$(envval CODEMAN_PASSWORD) fi AUTH=(); [ -n "${CODEMAN_PASSWORD:-}" ] && AUTH=(-u "${CODEMAN_USERNAME:-admin}:$CODEMAN_PASSWORD") # -k: harmless on http, required on https (self-signed cert). # X-Codeman-Parent-Session: tags workers YOU spawn as your children, so the web UI can # draw the lineage. Set once here and every present and future create call carries it. # The agent inbox reads it too, as "who is calling" (§5.15); every other endpoint ignores # it. It can never fail a spawn, so there is no case where you would want to leave it off. # X-Codeman-Agent-Origin: marks a case directory a spawn CREATES as agent scratch, so the # user can find and delete it long after your workers are gone (§5.14). Same deal: set # once, cosmetic, never fails a spawn, and it labels only directories Codeman creates. CURL=(curl -sk "${AUTH[@]}" -H "X-Codeman-Parent-Session: $SELF" -H "X-Codeman-Agent-Origin: codeman-skill") CID=codeman-agent-1 # FIXED literal, never "agent-$$": see below # Fail-CLOSED session delete. The DELETE lives INSIDE the guard on purpose: the older # `is_self "$SID" || curl -X DELETE ...` shape failed OPEN, because an undefined # is_self exits 127 and the `||` branch then ran the delete completely unguarded. # Undefined delete_session is "command not found", which deletes nothing. delete_session() { local id="${1:-}" [ -n "$id" ] || { echo "refusing: empty session id"; return 1; } [ "${#SELF}" -ge 8 ] || { echo "refusing: \$SELF unset or too short to prove this is not me"; return 1; } # ids appear in full AND 8-char form (Docker exports a truncated $SELF; mux names and # UI surfaces carry 8-char ids), so compare by prefix in BOTH directions. Equality or # a one-directional check each miss a real combination, and the miss deletes you. case "$id" in "$SELF"*) echo "refusing: $id is me"; return 1 ;; esac case "$SELF" in "$id"*) echo "refusing: $id is me"; return 1 ;; esac "${CURL[@]}" -X DELETE "$API/api/v1/sessions/$id" } # ---- fast path: the four verbs, already written. §1 composes them. ---- _composer_up() { # -> "true"/"false". `shift+tab` is the one token "${CURL[@]}" -G "$API/api/v1/sessions/${1}/wait-output" \ --data-urlencode 'match=shift+tab' --data-urlencode 'from=buffer' \ --data-urlencode "timeout=${2}" | jq -r '.data.wait.matched // false' } _mark_up() { # -> "true"/"false": a literal, space-free token # in the pane's output, read from the buffer so a composer drawn before the wait counts. "${CURL[@]}" -G "$API/api/v1/sessions/${1}/wait-output" \ --data-urlencode "match=${2}" --data-urlencode 'from=buffer' \ --data-urlencode "timeout=${3}" | jq -r '.data.wait.matched // false' } _dsh_up() { # -> "true"/"false". The DeepSeek Harness TUI's # composer glyph. Override with DSH_READY_MARK for a profile that draws another one. _mark_up "${1}" "${DSH_READY_MARK:-❯}" "${2}" } _dsh_guide() { # -> 0 when the pane shows dsh-TUI's first-run SETUP GUIDE, not a composer. # dsh-TUI 0.19.0 opens a guide ("Welcome to dsh-TUI Step 1 of 4") in every session until a # person has finished it once, and the guide's own cursor is `❯`: the readiness mark matches # on it (measured, 2 of 2 launches), and an Enter there acts on the highlighted step, which # is a sign-in. So a worker whose mark matched is still checked for the guide, on the # RENDERED pane with its whitespace removed, and never handed back while it shows. "${CURL[@]}" -G "$API/api/v1/sessions/${1}/terminal" --data-urlencode 'full=1' \ | jq -r '.data.terminalBuffer // empty' \ | sed -e "s/$(printf '\033')\[[0-9;?]*[a-zA-Z]//g" -e "s/$(printf '\033')[()][AB0]//g" \ | tr -d ' \t' | grep -qi 'Step[0-9]of[0-9]' } # ---- the workspace-trust dialog: READ the screen, never press Enter blind ---- # Claude Code 2.1.252 dropped the option numbers, REVERSED them, and highlights # "No, exit" by default: # Security guide # ❯ No, exit # Yes, I trust this folder # Enter to confirm . Esc to cancel # so the bare \r that answered the old layout now answers *exit* and the pane is # dead (`status 1`) seconds after the spawn -- measured on a live 2.1.252 case. # These two read the rendered pane and steer onto the trust option instead. _trust_key() { # -> "confirm" | "move" | "" (nothing safe to press) # full=1 returns the RENDERED pane; a claude pane keeps no tmux history, so that # is the current frame rather than every repaint since launch. tail -1 anyway, # because the freshest marked row is the only one still true. "${CURL[@]}" -G "$API/api/v1/sessions/${1}/terminal" --data-urlencode 'full=1' \ | jq -r '.data.terminalBuffer // empty' \ | sed -e "s/$(printf '\033')\[[0-9;?]*[a-zA-Z]//g" -e "s/$(printf '\033')[()][AB0]//g" \ | tr -d ' \t' | grep -i '❯[0-9.]*\(yes,itrustthisfolder\|no,exit\)' | tail -1 \ | sed -e 's/.*[Yy]es,.*/confirm/' -e 's/.*[Nn]o,.*/move/' } # ---- the composer: is the prompt still sitting there, unsent? ---- # ⚠️ Claude Code 2.1.277 (auto-installed 2026-09-18) takes typed text the moment the # composer paints but IGNORES Enter for the first 30-50 seconds after it: the \r that # Codeman sends 50 ms after the text and a lone nudge at 20 s both leave the prompt # stranded, with `0 tokens`, while the wait burns its whole timeout. Measured through # this very route: Enter at 28 s stranded, Enter at 51 s submitted. So sendwait READS # the composer and keeps pressing Enter while the prompt is still there. _composer_text() { # -> the composer's text with ALL whitespace removed: "" once # the prompt was taken, "?" when the pane shows no composer at all. The composer is # the LAST `❯` line: Claude Code echoes a submitted prompt with the same glyph higher # up in the transcript, so only the last one says whether the text was taken. local t t=$("${CURL[@]}" -G "$API/api/v1/sessions/${1}/terminal" --data-urlencode 'full=1' \ | jq -r '.data.terminalBuffer // empty' \ | sed -e "s/$(printf '\033')\[[0-9;?]*[a-zA-Z]//g" -e "s/$(printf '\033')[()][AB0]//g" \ | tr -d '\r' | grep -a '^[[:space:]]*❯' | tail -1) [ -n "$t" ] || { printf '?'; return 0; } # Claude Code draws a NO-BREAK SPACE (U+00A0) after the glyph, which [:space:] does # not cover, so it is stripped by its bytes, portably (BSD sed has no \xHH). printf '%s' "$t" | sed 's/^[[:space:]]*❯//' | tr -d '[:space:]' | sed "s/$(printf '\302\240')//g" } _accept_trust() { # -> 0 once it has answered the dialog, 1 if it could not local sid="${1}" k i=1 while [ "$i" -le 6 ]; do k=$(_trust_key "$sid") [ -n "$k" ] || return 1 # no dialog on screen, or a layout this cannot read # A SEPARATE clientId for these keys. seq is monotonic per clientId, so # spending prompt numbers here would make the next sendwait -- whose default # seq is the epoch second -- look like a stale duplicate and vanish silently. "${CURL[@]}" -X POST "$API/api/v1/sessions/$sid/input" -H 'Content-Type: application/json' \ -d "$(jq -nc --arg k "$([ "$k" = confirm ] && printf '\r' || printf '\033[B')" \ --arg c "$CID-trust-$sid" --argjson s "$i" \ '{input:$k,useMux:true,clientId:$c,seq:$s}')" >/dev/null [ "$k" = confirm ] && return 0 sleep 1; i=$((i+1)) # re-read: the arrow is CONFIRMED before Enter goes out done return 1 } # ---- the local model: the model Codeman installed on this Mac, as a worker ---- # local_model -> ONE compact JSON object on stdout. rc 0 only when a `local` worker can be # spawned now (`running:true`) or once spawn_worker has started the runtime (`running:false`): # {"spawnable":true,"state":"installed","running":true,"cli":"opencode","cliLabel":"OpenCode", # "endpointId":"local-ollama","modelId":"","modelLabel":"