diff --git a/plugins/codeman/skills/codeman/SKILL.md b/plugins/codeman/skills/codeman/SKILL.md index 3aebe247..c6ffbf58 100644 --- a/plugins/codeman/skills/codeman/SKILL.md +++ b/plugins/codeman/skills/codeman/SKILL.md @@ -197,6 +197,17 @@ _dsh_up() { # -> "true"/"false". The DeepSeek Harness T # 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: @@ -455,11 +466,17 @@ spawn_worker() { fi if [ -n "$cm" ] && [ "$mode" != claude ]; then # No trust dialog and no hook file to vet. opencode and pi have no end-of-turn signal, - # which is exactly why sendwait refuses them (400) and sendmark drives them; a deepseek - # worker reports the end of a turn on the local model too, so sendwait works for it. + # which is exactly why sendwait refuses them (400) and sendmark drives them. A deepseek + # worker reports the end of a turn on the local model too, but sendmark is still the way + # to get its ANSWER: dsh 0.2.2 renamed its transcript file, and until Codeman reads the + # new one `last-response` comes back empty for it. r=$(_mark_up "$sid" "$mark" 45000) [ "$r" = true ] || { echo "local worker $sid ($mode) never drew '$mark' within 45 s; deleted it. Look at GET /api/v1/local-model and the pane of a hand-made session before retrying" >&2 delete_session "$sid" >/dev/null; return 1; } + if [ "$mode" = deepseek ] && _dsh_guide "$sid"; then + echo "dsh worker $sid opened DeepSeek Harness's setup guide instead of a composer; deleted it. A person finishes that guide once (start a DeepSeek session from the Run menu), then spawn again" >&2 + delete_session "$sid" >/dev/null; return 1 + fi printf '%s\n' "$sid"; return 0 fi if [ "$mode" = deepseek ]; then @@ -475,6 +492,10 @@ spawn_worker() { r=$(_dsh_up "$sid" 45000) [ "$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 delete_session "$sid" >/dev/null; return 1; } + if _dsh_guide "$sid"; then + echo "dsh worker $sid opened DeepSeek Harness's setup guide instead of a composer; deleted it. A person finishes that guide once (start a DeepSeek session from the Run menu), then spawn again" >&2 + delete_session "$sid" >/dev/null; return 1 + fi printf '%s\n' "$sid"; return 0 fi [ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # no other mode draws a composer to wait on @@ -909,10 +930,10 @@ on), and from then on it is the same verbs. It mixes into one fleet like any oth | Worker kind | Agent | Why you would pick it | Drive it with | | --- | --- | --- | --- | -| `local` | the default one (`cli` of `local_model`) | you have no reason to choose | `sendmark` (`sendwait` when it is claude or deepseek) | +| `local` | the default one (`cli` of `local_model`) | you have no reason to choose | `sendmark` (`sendwait` when it is claude) | | `local:opencode` | OpenCode | the tested pair; a full tool set | `sendmark` | | `local:pi` | Pi | the smallest fixed prompt (about 2,100 tokens against 8,000 and more), so the first answer comes soonest and the most context is left for the work | `sendmark` | -| `local:deepseek` | DeepSeek Harness | the one of these that reports the end of a turn | `sendwait` or `sendmark` | +| `local:deepseek` | DeepSeek Harness | the user already works with it | `sendmark` (it reports the end of a turn, but on dsh 0.2.2 its answer cannot be read back through `sendwait` yet) | | `local:claude` | Claude Code | hooks and `sendwait`, at the price of a first answer that takes minutes | `sendwait` | An agent whose status is `installable`, `installing` or `setup-needed` is not startable, and @@ -968,8 +989,14 @@ The limits, before you promise the user anything: loads the model first. A local worker run by claude spends minutes on its first answer (its fixed prompt is about 36,000 tokens). - **Markers, not hooks.** OpenCode and Pi report no end of turn, so `sendwait` answers 400 - for those workers; `sendmark` is the way through. A claude worker has hooks and a - DeepSeek Harness worker reports its turns, so `sendwait` works for those two. + for those workers; `sendmark` is the way through. A claude worker has hooks, so + `sendwait` works for it. A DeepSeek Harness worker reports its turns too, but drive it + with `sendmark` on the local model: dsh 0.2.2 renamed its transcript file, and Codeman + reads an empty answer from it for now. +- **DeepSeek Harness has a setup guide a person finishes once.** Until then every dsh + session opens on it, and the spawn refuses the worker (and deletes it) instead of typing + a task into a sign-in screen. Tell the user to start one DeepSeek session from the Run + menu and finish the guide. - **Pi starts without trusting the folder.** A `local:pi` worker is started with `--no-approve`, so it loads no project extensions or skills from the case folder: on the local model Pi would otherwise ask to trust the folder in every session, and a task diff --git a/plugins/codeman/skills/codeman/preamble.sh b/plugins/codeman/skills/codeman/preamble.sh index 88a42ce7..a0fab818 100644 --- a/plugins/codeman/skills/codeman/preamble.sh +++ b/plugins/codeman/skills/codeman/preamble.sh @@ -59,6 +59,17 @@ _dsh_up() { # -> "true"/"false". The DeepSeek Harness T # 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: @@ -317,11 +328,17 @@ spawn_worker() { fi if [ -n "$cm" ] && [ "$mode" != claude ]; then # No trust dialog and no hook file to vet. opencode and pi have no end-of-turn signal, - # which is exactly why sendwait refuses them (400) and sendmark drives them; a deepseek - # worker reports the end of a turn on the local model too, so sendwait works for it. + # which is exactly why sendwait refuses them (400) and sendmark drives them. A deepseek + # worker reports the end of a turn on the local model too, but sendmark is still the way + # to get its ANSWER: dsh 0.2.2 renamed its transcript file, and until Codeman reads the + # new one `last-response` comes back empty for it. r=$(_mark_up "$sid" "$mark" 45000) [ "$r" = true ] || { echo "local worker $sid ($mode) never drew '$mark' within 45 s; deleted it. Look at GET /api/v1/local-model and the pane of a hand-made session before retrying" >&2 delete_session "$sid" >/dev/null; return 1; } + if [ "$mode" = deepseek ] && _dsh_guide "$sid"; then + echo "dsh worker $sid opened DeepSeek Harness's setup guide instead of a composer; deleted it. A person finishes that guide once (start a DeepSeek session from the Run menu), then spawn again" >&2 + delete_session "$sid" >/dev/null; return 1 + fi printf '%s\n' "$sid"; return 0 fi if [ "$mode" = deepseek ]; then @@ -337,6 +354,10 @@ spawn_worker() { r=$(_dsh_up "$sid" 45000) [ "$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 delete_session "$sid" >/dev/null; return 1; } + if _dsh_guide "$sid"; then + echo "dsh worker $sid opened DeepSeek Harness's setup guide instead of a composer; deleted it. A person finishes that guide once (start a DeepSeek session from the Run menu), then spawn again" >&2 + delete_session "$sid" >/dev/null; return 1 + fi printf '%s\n' "$sid"; return 0 fi [ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # no other mode draws a composer to wait on diff --git a/plugins/codeman/skills/codeman/reference/verbs.md b/plugins/codeman/skills/codeman/reference/verbs.md index 85968846..709e3b76 100644 --- a/plugins/codeman/skills/codeman/reference/verbs.md +++ b/plugins/codeman/skills/codeman/reference/verbs.md @@ -893,7 +893,7 @@ confirms in the window, and no route this skill calls can do it. | Agent | Token | What was measured | |-------|-------|-------------------| | OpenCode | `┃` | 1.3.0 and 1.19.5: nothing draws it before the composer, no first-run screen, and a prompt pasted at the first `┃` was submitted (2 of 2 on 1.19.5) | - | DeepSeek Harness | `❯` | 0.1.1-rc.2 with dsh-TUI 0.10.0-beta.1: splash, composer and status line arrive in one frame; a prompt sent at the mark was submitted (2 of 2). The harness reports idle about 150 ms BEFORE that frame, so `stop` is no readiness signal | + | DeepSeek Harness | `❯`, then a look at the pane | 0.1.1-rc.2 with dsh-TUI 0.10.0-beta.1: splash, composer and status line arrive in one frame; a prompt sent at the mark was submitted (2 of 2). The harness reports idle about 150 ms BEFORE that frame, so `stop` is no readiness signal. 0.2.2-rc.1 with dsh-TUI 0.19.0: a setup guide ("Step 1 of 4") opens in every session until a person has finished it once, and its cursor is `❯` too (the mark matched on it, 2 of 2), so after the mark the rendered pane is read and a worker that shows the guide is deleted, never handed back; with the guide finished, a prompt sent at the mark was submitted | | Pi | `extended-keys` | 1.5.2: Pi paints its composer rules in its first frame and ignores Enter until startup has finished, so the rule `─` is NOT a mark (a prompt sent at it stayed in the composer, 2 of 2). The last line of its startup is a warning that tmux's `extended-keys` option is off, which it is on Codeman's socket; a prompt sent once that word showed was submitted (4 of 4). A side effect, not a contract: a Pi that stops printing it fails the spawn until `LOCAL_READY_MARK` names what it draws instead | A claude agent takes the claude path instead (hooks vetted, `shift+tab`, the trust diff --git a/skills/codeman/SKILL.md b/skills/codeman/SKILL.md index 3aebe247..c6ffbf58 100644 --- a/skills/codeman/SKILL.md +++ b/skills/codeman/SKILL.md @@ -197,6 +197,17 @@ _dsh_up() { # -> "true"/"false". The DeepSeek Harness T # 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: @@ -455,11 +466,17 @@ spawn_worker() { fi if [ -n "$cm" ] && [ "$mode" != claude ]; then # No trust dialog and no hook file to vet. opencode and pi have no end-of-turn signal, - # which is exactly why sendwait refuses them (400) and sendmark drives them; a deepseek - # worker reports the end of a turn on the local model too, so sendwait works for it. + # which is exactly why sendwait refuses them (400) and sendmark drives them. A deepseek + # worker reports the end of a turn on the local model too, but sendmark is still the way + # to get its ANSWER: dsh 0.2.2 renamed its transcript file, and until Codeman reads the + # new one `last-response` comes back empty for it. r=$(_mark_up "$sid" "$mark" 45000) [ "$r" = true ] || { echo "local worker $sid ($mode) never drew '$mark' within 45 s; deleted it. Look at GET /api/v1/local-model and the pane of a hand-made session before retrying" >&2 delete_session "$sid" >/dev/null; return 1; } + if [ "$mode" = deepseek ] && _dsh_guide "$sid"; then + echo "dsh worker $sid opened DeepSeek Harness's setup guide instead of a composer; deleted it. A person finishes that guide once (start a DeepSeek session from the Run menu), then spawn again" >&2 + delete_session "$sid" >/dev/null; return 1 + fi printf '%s\n' "$sid"; return 0 fi if [ "$mode" = deepseek ]; then @@ -475,6 +492,10 @@ spawn_worker() { r=$(_dsh_up "$sid" 45000) [ "$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 delete_session "$sid" >/dev/null; return 1; } + if _dsh_guide "$sid"; then + echo "dsh worker $sid opened DeepSeek Harness's setup guide instead of a composer; deleted it. A person finishes that guide once (start a DeepSeek session from the Run menu), then spawn again" >&2 + delete_session "$sid" >/dev/null; return 1 + fi printf '%s\n' "$sid"; return 0 fi [ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # no other mode draws a composer to wait on @@ -909,10 +930,10 @@ on), and from then on it is the same verbs. It mixes into one fleet like any oth | Worker kind | Agent | Why you would pick it | Drive it with | | --- | --- | --- | --- | -| `local` | the default one (`cli` of `local_model`) | you have no reason to choose | `sendmark` (`sendwait` when it is claude or deepseek) | +| `local` | the default one (`cli` of `local_model`) | you have no reason to choose | `sendmark` (`sendwait` when it is claude) | | `local:opencode` | OpenCode | the tested pair; a full tool set | `sendmark` | | `local:pi` | Pi | the smallest fixed prompt (about 2,100 tokens against 8,000 and more), so the first answer comes soonest and the most context is left for the work | `sendmark` | -| `local:deepseek` | DeepSeek Harness | the one of these that reports the end of a turn | `sendwait` or `sendmark` | +| `local:deepseek` | DeepSeek Harness | the user already works with it | `sendmark` (it reports the end of a turn, but on dsh 0.2.2 its answer cannot be read back through `sendwait` yet) | | `local:claude` | Claude Code | hooks and `sendwait`, at the price of a first answer that takes minutes | `sendwait` | An agent whose status is `installable`, `installing` or `setup-needed` is not startable, and @@ -968,8 +989,14 @@ The limits, before you promise the user anything: loads the model first. A local worker run by claude spends minutes on its first answer (its fixed prompt is about 36,000 tokens). - **Markers, not hooks.** OpenCode and Pi report no end of turn, so `sendwait` answers 400 - for those workers; `sendmark` is the way through. A claude worker has hooks and a - DeepSeek Harness worker reports its turns, so `sendwait` works for those two. + for those workers; `sendmark` is the way through. A claude worker has hooks, so + `sendwait` works for it. A DeepSeek Harness worker reports its turns too, but drive it + with `sendmark` on the local model: dsh 0.2.2 renamed its transcript file, and Codeman + reads an empty answer from it for now. +- **DeepSeek Harness has a setup guide a person finishes once.** Until then every dsh + session opens on it, and the spawn refuses the worker (and deletes it) instead of typing + a task into a sign-in screen. Tell the user to start one DeepSeek session from the Run + menu and finish the guide. - **Pi starts without trusting the folder.** A `local:pi` worker is started with `--no-approve`, so it loads no project extensions or skills from the case folder: on the local model Pi would otherwise ask to trust the folder in every session, and a task diff --git a/skills/codeman/preamble.sh b/skills/codeman/preamble.sh index 88a42ce7..a0fab818 100644 --- a/skills/codeman/preamble.sh +++ b/skills/codeman/preamble.sh @@ -59,6 +59,17 @@ _dsh_up() { # -> "true"/"false". The DeepSeek Harness T # 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: @@ -317,11 +328,17 @@ spawn_worker() { fi if [ -n "$cm" ] && [ "$mode" != claude ]; then # No trust dialog and no hook file to vet. opencode and pi have no end-of-turn signal, - # which is exactly why sendwait refuses them (400) and sendmark drives them; a deepseek - # worker reports the end of a turn on the local model too, so sendwait works for it. + # which is exactly why sendwait refuses them (400) and sendmark drives them. A deepseek + # worker reports the end of a turn on the local model too, but sendmark is still the way + # to get its ANSWER: dsh 0.2.2 renamed its transcript file, and until Codeman reads the + # new one `last-response` comes back empty for it. r=$(_mark_up "$sid" "$mark" 45000) [ "$r" = true ] || { echo "local worker $sid ($mode) never drew '$mark' within 45 s; deleted it. Look at GET /api/v1/local-model and the pane of a hand-made session before retrying" >&2 delete_session "$sid" >/dev/null; return 1; } + if [ "$mode" = deepseek ] && _dsh_guide "$sid"; then + echo "dsh worker $sid opened DeepSeek Harness's setup guide instead of a composer; deleted it. A person finishes that guide once (start a DeepSeek session from the Run menu), then spawn again" >&2 + delete_session "$sid" >/dev/null; return 1 + fi printf '%s\n' "$sid"; return 0 fi if [ "$mode" = deepseek ]; then @@ -337,6 +354,10 @@ spawn_worker() { r=$(_dsh_up "$sid" 45000) [ "$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 delete_session "$sid" >/dev/null; return 1; } + if _dsh_guide "$sid"; then + echo "dsh worker $sid opened DeepSeek Harness's setup guide instead of a composer; deleted it. A person finishes that guide once (start a DeepSeek session from the Run menu), then spawn again" >&2 + delete_session "$sid" >/dev/null; return 1 + fi printf '%s\n' "$sid"; return 0 fi [ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # no other mode draws a composer to wait on diff --git a/skills/codeman/reference/verbs.md b/skills/codeman/reference/verbs.md index 85968846..709e3b76 100644 --- a/skills/codeman/reference/verbs.md +++ b/skills/codeman/reference/verbs.md @@ -893,7 +893,7 @@ confirms in the window, and no route this skill calls can do it. | Agent | Token | What was measured | |-------|-------|-------------------| | OpenCode | `┃` | 1.3.0 and 1.19.5: nothing draws it before the composer, no first-run screen, and a prompt pasted at the first `┃` was submitted (2 of 2 on 1.19.5) | - | DeepSeek Harness | `❯` | 0.1.1-rc.2 with dsh-TUI 0.10.0-beta.1: splash, composer and status line arrive in one frame; a prompt sent at the mark was submitted (2 of 2). The harness reports idle about 150 ms BEFORE that frame, so `stop` is no readiness signal | + | DeepSeek Harness | `❯`, then a look at the pane | 0.1.1-rc.2 with dsh-TUI 0.10.0-beta.1: splash, composer and status line arrive in one frame; a prompt sent at the mark was submitted (2 of 2). The harness reports idle about 150 ms BEFORE that frame, so `stop` is no readiness signal. 0.2.2-rc.1 with dsh-TUI 0.19.0: a setup guide ("Step 1 of 4") opens in every session until a person has finished it once, and its cursor is `❯` too (the mark matched on it, 2 of 2), so after the mark the rendered pane is read and a worker that shows the guide is deleted, never handed back; with the guide finished, a prompt sent at the mark was submitted | | Pi | `extended-keys` | 1.5.2: Pi paints its composer rules in its first frame and ignores Enter until startup has finished, so the rule `─` is NOT a mark (a prompt sent at it stayed in the composer, 2 of 2). The last line of its startup is a warning that tmux's `extended-keys` option is off, which it is on Codeman's socket; a prompt sent once that word showed was submitted (4 of 4). A side effect, not a contract: a Pi that stops printing it fails the spawn until `LOCAL_READY_MARK` names what it draws instead | A claude agent takes the claude path instead (hooks vetted, `shift+tab`, the trust diff --git a/test/agent-skill-local-model.test.ts b/test/agent-skill-local-model.test.ts index 167a37f1..a0a961a5 100644 --- a/test/agent-skill-local-model.test.ts +++ b/test/agent-skill-local-model.test.ts @@ -551,6 +551,58 @@ describe.skipIf(!hasJq)('preamble spawn_worker local', () => { expect(run.calls[2].q).toContain('match=❯'); }); + it('a DeepSeek Harness worker that opened its setup guide is deleted, never handed back: its cursor is the mark too', () => { + // dsh-TUI 0.19.0, as the rendered pane reads: the guide, with `❯` on its first step. + const guide = JSON.stringify({ + success: true, + data: { + terminalBuffer: + '\u001b[1mWelcome to dsh-TUI\u001b[0m Step 1 of 4\r\n ✗ No DEEPSEEK_API_KEY yet\r\n❯ Sign in to a subscription (OAuth)\r\n', + }, + }); + const composer = JSON.stringify({ + success: true, + data: { + terminalBuffer: + '╭────╮\r\n│ ❯ Say something, or type / for commands…\r\n╰────╯\r\n qwen3.6:35b-a3b · standard\r\n', + }, + }); + const answers = { 'lm-get': WITH_AGENTS, 'quick-start': QUICK_START, 'wait-output': MATCHED }; + + const blocked = runSkill('spawn_worker rev local:deepseek', { + ...answers, + terminal: guide, + delete: JSON.stringify({ success: true, data: {} }), + }); + expect(blocked.rc).toBe(1); + expect(blocked.stdout).toBe(''); + expect(blocked.stderr).toMatch(/setup guide/); + expect(blocked.calls.map((call) => `${call.m} ${call.key}`)).toEqual([ + 'GET lm-get', + 'POST quick-start', + 'GET wait-output', + 'GET terminal', + 'DELETE delete', + ]); + + const ready = runSkill('spawn_worker rev local:deepseek', { ...answers, terminal: composer }); + expect(ready.stderr).toBe(''); + expect(ready.rc).toBe(0); + expect(ready.stdout).toBe(`${SID}\n`); + + // The same look for a DeepSeek worker that is not on the local model. + const cloud = runSkill('spawn_worker rev deepseek', { + 'quick-start': QUICK_START, + 'wait-output': MATCHED, + terminal: guide, + delete: JSON.stringify({ success: true, data: {} }), + }); + expect(cloud.rc).toBe(1); + expect(cloud.stdout).toBe(''); + expect(cloud.stderr).toMatch(/setup guide/); + expect(cloud.calls.map((call) => call.key)).toEqual(['quick-start', 'wait-output', 'terminal', 'delete']); + }); + it('a mark the server names wins over the one this skill measured, and LOCAL_READY_MARK over both', () => { const named = status({ ...INSTALLED,