From 28e8029ce5f0e93fb2b521ff700c24680a9ea027 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sun, 8 Nov 2026 07:48:32 +0100 Subject: [PATCH] docs(skill): Claude in the cloud, a local model on this Mac SKILL.md gets a subsection after the DeepSeek Harness workers: why to hand work to the model on this Mac (bulk work with no per-token bill, files that stay on the machine, a second opinion, a worker that needs no network), the copy-paste block (local_model, spawn_worker local, sendmark, delete_session), and the limits as the code has them: a person installs the model, one request at a time, slower than the cloud, markers not hooks, a spawn can start a runtime the person stopped, admin only and experimental. The description now names the local model, so "use the local model" reaches the skill. Section 2 gains a row, the hook-less row points at sendmark, and reference/verbs.md gets 5.16 with every local_model reason, the spawn steps and the sendmark exit codes. Co-Authored-By: Claude Opus 5.5 --- plugins/codeman/skills/codeman/SKILL.md | 75 ++++++++++++++- .../codeman/skills/codeman/reference/verbs.md | 94 ++++++++++++++++++- skills/codeman/SKILL.md | 75 ++++++++++++++- skills/codeman/reference/verbs.md | 94 ++++++++++++++++++- 4 files changed, 328 insertions(+), 10 deletions(-) diff --git a/plugins/codeman/skills/codeman/SKILL.md b/plugins/codeman/skills/codeman/SKILL.md index f1416871..8e16527f 100644 --- a/plugins/codeman/skills/codeman/SKILL.md +++ b/plugins/codeman/skills/codeman/SKILL.md @@ -7,7 +7,8 @@ description: >- CLI, send them prompts, block until they finish (wait / wait-output / send-and-wait), read their output, and clean up; where available, message claude workers directly (Claude Code cross-session messaging). Use when asked to orchestrate or parallelize - work across Codeman sessions, get a second opinion from another CLI or model, watch + work across Codeman sessions, get a second opinion from another CLI or model, hand + work to the local model Codeman installed on this Mac ("use the local model"), watch another session, start and manage workers, or retrieve saved local Codex history by file/native ID or root and descendants with topic/date filters. Live API operations require a Codeman-managed session (CODEMAN_MUX=1). Offline history @@ -763,8 +764,8 @@ the time went into deliberation, not the API. The four things that actually cost polled pid for nothing, and shipped its workers without lineage. - **Verifying what is already checked for you.** Two verifications specifically are not worth a call here, because `spawn_worker` carries them: the hooks check (it refuses a - name that resolved to a hook-less directory with one local grep, so a worker it hands - back always has a working `stop` and `sendwait` is trustworthy), and the pid poll, + name that resolved to a hook-less directory with one local grep, so a claude worker it + hands back always has a working `stop` and `sendwait` is trustworthy), and the pid poll, which is dead weight because `wait-output` already blocks on the composer. Four things this block leans on, each one link away, no detour needed to run it: @@ -824,6 +825,70 @@ Three things to know before you spawn one: worker whose pane clearly finished: that profile is one of them, so drive it with markers instead. +### Claude in the cloud, a local model on this Mac + +When Codeman has installed a model on the Mac it runs on (the **Run a local model** button +on its welcome screen), you can hand work to that model from here. You stay the planner and +the reviewer in the cloud; the Mac does the bulk. `spawn_worker local` starts a worker +on the installed model, run by the coding agent it was installed with (OpenCode on the Mac +it was tested on), and from then on it is the same verbs. It mixes into one fleet like any +other mode: `spawn_workers plan review:deepseek summ:local`. + +Reach for it when: + +- **The work is bulk, not judgment**: summarizing a folder, classifying, first drafts of + tests, boilerplate. A local turn has no per-token bill. +- **The files should stay on this machine**: the worker reads them here, and only what you + read back from it enters your own context. +- **You want a second opinion from a different model**, with different blind spots. +- **The worker should not depend on the network**: the model's runtime listens on this Mac + only, started with its cloud features off. You, the lead, still need a connection. + +```bash +. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader +[ "${CODEMAN_PREAMBLE:-}" = 1.50.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; } +local_model || exit 1 # one JSON line: model, agent, endpoint; or the reason there is none +SID=$(spawn_worker summ local) || exit 1 # starts a stopped runtime first; empty = failed +WD=$("${CURL[@]}" "$API/api/v1/sessions/$SID" | jq -r .data.workingDir) +cp ./docs/*.md "$WD/" # its input goes inside its own folder (§5.9) +R=$(sendmark "$SID" 'Summarize each .md file in this folder in two lines' 900); RC=$? +jq -r '.answer // "(no answer)"' <<<"$R" +if [ "$RC" = 0 ]; then delete_session "$SID" >/dev/null +else echo "kept $SID (sendmark rc $RC: 2 no marker yet, 3 ended, 4 no answer file): $R"; fi +``` + +`sendmark` is the local worker's `sendwait`: it adds one sentence to your prompt asking for +the answer in `codeman-answer-.md` in the worker's folder and then a split marker, +waits for the marker with `wait-output`, and reads the file back +([§5.16](reference/verbs.md#516-a-worker-on-the-local-model)). Read its `answer`, not the +pane: a full-screen agent's pane is a poor transcript. ⚠️ A marker proves that the model +printed a token, not that the work is done or right. Review what comes back the way you +would review any first draft. + +The limits, before you promise the user anything: + +- **A Mac with Apple silicon, and a model a person installed** in Codeman's local model + window. This skill never installs one: that is a download of many gigabytes the person + confirms there. `local_model` says what is missing (`not-mac`, `not-installed`, + `install-unfinished`, `blocked`, `admin-only`, `unsupported`), so tell the user that + instead of guessing. +- **One model, one request at a time.** The runtime loads one model and answers one request + at a time, so two local workers take turns rather than work side by side. Fan out to the + cloud; give the Mac a queue. +- **Slower than a cloud model.** The one measured pair, OpenCode with Qwen3.6 35B-A3B on an + M5 Pro with 48 GB, ran at about 68 tokens a second, and the first request after a start + 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 reports no end of turn, so `sendwait` answers 400 for + that worker; `sendmark` is the way through. If the installed agent is claude, the worker + has hooks and `sendwait` works as for any claude worker. +- **Spawning can change what the person set.** A stopped runtime is started, which also + undoes the person's Stop: it then starts with Codeman again. And while a local worker is + open, the window refuses Stop and Remove and names your workers, so delete them as soon + as they are done. +- **Admin only in multi-user mode, and experimental.** The local model routes answer 403 + for anyone else (`admin-only`), and they may change in any release. + ## 2. What do you want to do? One row per job. Acting on this table alone is correct; the §5 links are the detail. @@ -833,7 +898,8 @@ One row per job. Acting on this table alone is correct; the §5 links are the de | start a worker **where the work is** | `POST /api/v1/quick-start {"caseName":…}`, which **creates** `~/codeman-cases/` 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) | | 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) | | 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) | -| 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) | +| 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. For an agent worker, `sendmark ""` does all of it and reads the answer back | [§5.5](reference/verbs.md#55-markers-for-hook-less-workers) | +| hand work to the local model on this Mac (cheap bulk work, files that stay here, a second opinion) | `local_model` says what is installed; `spawn_worker local` (or `name:local` in `spawn_workers`) starts a worker on it; `sendmark` tasks it and returns the answer | [Local model](#claude-in-the-cloud-a-local-model-on-this-mac), [§5.16](reference/verbs.md#516-a-worker-on-the-local-model) | | 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) | | 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) | | 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) | @@ -949,6 +1015,7 @@ these**; open the one row you actually hit. | [5.13 Messaging claude workers](reference/verbs.md#513-messaging-claude-workers) | `ListAgents` / `SendMessage` instead of the HTTP path | | [5.14 Clean up](reference/verbs.md#514-clean-up) | what deleting a session does **not** remove, and how to list the case dirs you left | | [5.15 Agent inbox](reference/verbs.md#515-agent-inbox) | messages that wait for the receiver instead of being typed: post, read until ack, who counts as the receiver | +| [5.16 A worker on the local model](reference/verbs.md#516-a-worker-on-the-local-model) | what `local_model` answers, what `spawn_worker local` does step by step, and every `sendmark` outcome | ## 6. Setup and auth diff --git a/plugins/codeman/skills/codeman/reference/verbs.md b/plugins/codeman/skills/codeman/reference/verbs.md index d72d46bc..3a18a70d 100644 --- a/plugins/codeman/skills/codeman/reference/verbs.md +++ b/plugins/codeman/skills/codeman/reference/verbs.md @@ -484,7 +484,10 @@ snippet carries the exit code back to you. For a **claude** worker with no hooks, ask for the marker in halves in the prompt itself ("print the word WORKDONE immediately followed by `_`") for the same -reason, and match the joined token. ⚠️ Against a TUI, match a single space-free token: +reason, and match the joined token. For any **agent** worker without `stop` (a hook-less +claude, opencode, gemini, pi, grok, omp, copilot, a worker on the local model), the +preamble's `sendmark ""` is exactly that in one call, and also asks for the +answer in a file it reads back: [§5.16](#516-a-worker-on-the-local-model). ⚠️ Against a TUI, match a single space-free token: a full-screen TUI positions text with cursor movements rather than literal spaces, so the stripped stream can read `Yes,Itrustthisfolder`, and whether a phrase keeps its spaces depends on how the TUI happened to draw it (observed live: some match, some @@ -814,6 +817,95 @@ Rules that matter: - **Lifetime.** An inbox goes with its session on delete. A server restart empties all inboxes unless the user turned on *Keep agent inbox across restarts* in Settings. +### 5.16 A worker on the local model + +The detail behind [Claude in the cloud, a local model on this +Mac](../SKILL.md#claude-in-the-cloud-a-local-model-on-this-mac). Three preamble functions, +all read from source and pinned by `test/agent-skill-local-model.test.ts` against a fake +server; the local model routes themselves are experimental. + +**`local_model`** is one `GET /api/v1/local-model` (the `/api/v1` alias reaches the +experimental route like any other) and prints one JSON line. Exit 0 means a `local` worker +can be spawned now (`running: true`) or once `spawn_worker` has started the runtime +(`running: false`): + +```json +{"spawnable":true,"state":"installed","running":true,"cli":"opencode","cliLabel":"OpenCode", + "endpointId":"local-ollama","modelId":"qwen3.6:35b-a3b","modelLabel":"Qwen3.6 35B-A3B"} +``` + +Exit 1 carries `reason` and `message` (the server's own sentence where it has one): + +| `reason` | What it means | What to tell the user | +|----------|---------------|-----------------------| +| `not-mac` | the server is not on macOS | local models need a Mac with Apple silicon | +| `not-installed` | nothing installed; on a Mac where no model fits, `message` says why | install one with **Run a local model** on the welcome screen | +| `install-unfinished` | a job is installing, paused or failed (`state`, `step`, `errorCode`) | finish it in that window; nothing here continues it | +| `blocked` | installed, but the server refuses a session now (`errorCode`: `model-changed`, `port-in-use`, `runtime-start-failed`, `runtime-stopped`) | the window shows the one button that helps | +| `admin-only` | multi-user mode, and you are not an admin (403) | ask an admin | +| `unsupported` | the server has no local model routes (404) | it predates the feature | +| `unauthorized`, `unreachable`, `error` | the request itself failed | §6 for a 401 | + +It never downloads, installs or starts anything. Installing is many gigabytes the person +confirms in the window, and no route this skill calls can do it. + +**`spawn_worker local`** (also `name:local` in `spawn_workers`), in order: + +1. `local_model`; anything but exit 0 fails the spawn with that reason. +2. Runtime stopped: `POST /api/v1/local-model/runtime {"action":"start"}`, what the + window's Start button sends, then `local_model` again until it reports `running` + (about 30 s at most). This undoes a Stop the person made. Concurrent spawns may both + send it; the server runs one runtime verb at a time and keeps a runtime that runs. +3. `POST /api/v1/quick-start {"caseName":…,"mode":"","parentSessionId":…, + "customModel":{"endpointId":…,"modelId":…}}`: the launch of the window's "Start a + session", with the installed agent as the mode. No advisor, which is a cloud model. + The server checks again that its own runtime runs and that the model on disk is the one + it installed, and refuses otherwise (`OPERATION_FAILED`, with the reason). +4. An answer of `requiresContextWarning` or `requiresConfirmation` is a question for a + person (a context below the agent's safe floor, a model swap that would evict another + session's model). It fails the spawn with the answer on stderr and nothing on stdout, + and no `confirmed*` flag is ever sent. +5. Readiness: OpenCode's composer bar `┃` (its registry `promptGlyph`) through + `wait-output`, 45 s, and the session deleted on a miss. A claude agent takes the claude + path instead (hooks vetted, `shift+tab`, the trust dialog). Any other agent needs + `LOCAL_READY_MARK` set to a space-free token its composer draws, or the spawn is refused + before a session exists. Not measured here: an OpenCode first-run screen that hides the + composer would read as a miss. + +**`sendmark [timeoutSec]`** drives it, and any other agent worker without +an end-of-turn signal ([§5.5](#55-markers-for-hook-less-workers)): + +- It joins the prompt onto one line and adds one sentence: write the complete answer to + `codeman-answer-.md` in the working directory, and only then end the reply with + the word DONE immediately followed by `_`. The typed text never holds the joined + marker, so its echo cannot match. +- It sends once with the exactly-once body (`$CID-`, an epoch-second `seq`, one `\r`) + and a one-second `exit` wait, so `delivered` is the pane's answer, not a guess. +- It waits on `wait-output?match=DONE_&from=buffer` in rounds of at most 60 s until + the timeout (default 600 s), then reads the answer file from this machine's disk: only a + regular file (a symlink is refused), at most 256 KB. + +| exit | JSON | Meaning | +|------|------|---------| +| 0 | `answer` holds the file | the marker printed and the file was read | +| 2 | `matched: false` | no marker before the timeout. The worker may still be working: re-check the pane, never resend blindly | +| 3 | `ended: true` or `delivered: false` | the worker's pane ended | +| 4 | `matched: true, answer: null` | the marker printed, but no file within `SENDMARK_FILE_WAIT` s (default 120): the model ignored the instruction, or a reasoning model restated the marker while planning and is still at work | +| 1 | `detail` | the send or a wait failed | + +What a marker proves, and what it does not: the model printed the token. Not that the +work is done, not that it is right, and with a reasoning model not even that the turn is +over, which is why the file is the second gate. What a small local model writes is a +first draft; review it before you build on it. The answer files stay in the worker's +folder, a scratch case that [§5.14](#514-clean-up) lists for cleanup. +`SENDMARK_ANSWER_FILE=0` asks for the marker alone, for a worker that cannot write where +it runs (codex in a folder it runs read-only, an agent that asks before it writes a file, +a worker on another host): exit 0 then means only that the marker printed. A worker that +stops on such a question shows up here as exit 2, not as an error. + +Close every local worker you opened: while one is open the window refuses Stop and +Remove, naming it. + ## Experimental bounded inspection and guarded submission These three commands and their HTTP routes are labeled experimental, so they may diff --git a/skills/codeman/SKILL.md b/skills/codeman/SKILL.md index f1416871..8e16527f 100644 --- a/skills/codeman/SKILL.md +++ b/skills/codeman/SKILL.md @@ -7,7 +7,8 @@ description: >- CLI, send them prompts, block until they finish (wait / wait-output / send-and-wait), read their output, and clean up; where available, message claude workers directly (Claude Code cross-session messaging). Use when asked to orchestrate or parallelize - work across Codeman sessions, get a second opinion from another CLI or model, watch + work across Codeman sessions, get a second opinion from another CLI or model, hand + work to the local model Codeman installed on this Mac ("use the local model"), watch another session, start and manage workers, or retrieve saved local Codex history by file/native ID or root and descendants with topic/date filters. Live API operations require a Codeman-managed session (CODEMAN_MUX=1). Offline history @@ -763,8 +764,8 @@ the time went into deliberation, not the API. The four things that actually cost polled pid for nothing, and shipped its workers without lineage. - **Verifying what is already checked for you.** Two verifications specifically are not worth a call here, because `spawn_worker` carries them: the hooks check (it refuses a - name that resolved to a hook-less directory with one local grep, so a worker it hands - back always has a working `stop` and `sendwait` is trustworthy), and the pid poll, + name that resolved to a hook-less directory with one local grep, so a claude worker it + hands back always has a working `stop` and `sendwait` is trustworthy), and the pid poll, which is dead weight because `wait-output` already blocks on the composer. Four things this block leans on, each one link away, no detour needed to run it: @@ -824,6 +825,70 @@ Three things to know before you spawn one: worker whose pane clearly finished: that profile is one of them, so drive it with markers instead. +### Claude in the cloud, a local model on this Mac + +When Codeman has installed a model on the Mac it runs on (the **Run a local model** button +on its welcome screen), you can hand work to that model from here. You stay the planner and +the reviewer in the cloud; the Mac does the bulk. `spawn_worker local` starts a worker +on the installed model, run by the coding agent it was installed with (OpenCode on the Mac +it was tested on), and from then on it is the same verbs. It mixes into one fleet like any +other mode: `spawn_workers plan review:deepseek summ:local`. + +Reach for it when: + +- **The work is bulk, not judgment**: summarizing a folder, classifying, first drafts of + tests, boilerplate. A local turn has no per-token bill. +- **The files should stay on this machine**: the worker reads them here, and only what you + read back from it enters your own context. +- **You want a second opinion from a different model**, with different blind spots. +- **The worker should not depend on the network**: the model's runtime listens on this Mac + only, started with its cloud features off. You, the lead, still need a connection. + +```bash +. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader +[ "${CODEMAN_PREAMBLE:-}" = 1.50.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; } +local_model || exit 1 # one JSON line: model, agent, endpoint; or the reason there is none +SID=$(spawn_worker summ local) || exit 1 # starts a stopped runtime first; empty = failed +WD=$("${CURL[@]}" "$API/api/v1/sessions/$SID" | jq -r .data.workingDir) +cp ./docs/*.md "$WD/" # its input goes inside its own folder (§5.9) +R=$(sendmark "$SID" 'Summarize each .md file in this folder in two lines' 900); RC=$? +jq -r '.answer // "(no answer)"' <<<"$R" +if [ "$RC" = 0 ]; then delete_session "$SID" >/dev/null +else echo "kept $SID (sendmark rc $RC: 2 no marker yet, 3 ended, 4 no answer file): $R"; fi +``` + +`sendmark` is the local worker's `sendwait`: it adds one sentence to your prompt asking for +the answer in `codeman-answer-.md` in the worker's folder and then a split marker, +waits for the marker with `wait-output`, and reads the file back +([§5.16](reference/verbs.md#516-a-worker-on-the-local-model)). Read its `answer`, not the +pane: a full-screen agent's pane is a poor transcript. ⚠️ A marker proves that the model +printed a token, not that the work is done or right. Review what comes back the way you +would review any first draft. + +The limits, before you promise the user anything: + +- **A Mac with Apple silicon, and a model a person installed** in Codeman's local model + window. This skill never installs one: that is a download of many gigabytes the person + confirms there. `local_model` says what is missing (`not-mac`, `not-installed`, + `install-unfinished`, `blocked`, `admin-only`, `unsupported`), so tell the user that + instead of guessing. +- **One model, one request at a time.** The runtime loads one model and answers one request + at a time, so two local workers take turns rather than work side by side. Fan out to the + cloud; give the Mac a queue. +- **Slower than a cloud model.** The one measured pair, OpenCode with Qwen3.6 35B-A3B on an + M5 Pro with 48 GB, ran at about 68 tokens a second, and the first request after a start + 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 reports no end of turn, so `sendwait` answers 400 for + that worker; `sendmark` is the way through. If the installed agent is claude, the worker + has hooks and `sendwait` works as for any claude worker. +- **Spawning can change what the person set.** A stopped runtime is started, which also + undoes the person's Stop: it then starts with Codeman again. And while a local worker is + open, the window refuses Stop and Remove and names your workers, so delete them as soon + as they are done. +- **Admin only in multi-user mode, and experimental.** The local model routes answer 403 + for anyone else (`admin-only`), and they may change in any release. + ## 2. What do you want to do? One row per job. Acting on this table alone is correct; the §5 links are the detail. @@ -833,7 +898,8 @@ One row per job. Acting on this table alone is correct; the §5 links are the de | start a worker **where the work is** | `POST /api/v1/quick-start {"caseName":…}`, which **creates** `~/codeman-cases/` 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) | | 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) | | 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) | -| 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) | +| 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. For an agent worker, `sendmark ""` does all of it and reads the answer back | [§5.5](reference/verbs.md#55-markers-for-hook-less-workers) | +| hand work to the local model on this Mac (cheap bulk work, files that stay here, a second opinion) | `local_model` says what is installed; `spawn_worker local` (or `name:local` in `spawn_workers`) starts a worker on it; `sendmark` tasks it and returns the answer | [Local model](#claude-in-the-cloud-a-local-model-on-this-mac), [§5.16](reference/verbs.md#516-a-worker-on-the-local-model) | | 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) | | 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) | | 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) | @@ -949,6 +1015,7 @@ these**; open the one row you actually hit. | [5.13 Messaging claude workers](reference/verbs.md#513-messaging-claude-workers) | `ListAgents` / `SendMessage` instead of the HTTP path | | [5.14 Clean up](reference/verbs.md#514-clean-up) | what deleting a session does **not** remove, and how to list the case dirs you left | | [5.15 Agent inbox](reference/verbs.md#515-agent-inbox) | messages that wait for the receiver instead of being typed: post, read until ack, who counts as the receiver | +| [5.16 A worker on the local model](reference/verbs.md#516-a-worker-on-the-local-model) | what `local_model` answers, what `spawn_worker local` does step by step, and every `sendmark` outcome | ## 6. Setup and auth diff --git a/skills/codeman/reference/verbs.md b/skills/codeman/reference/verbs.md index d72d46bc..3a18a70d 100644 --- a/skills/codeman/reference/verbs.md +++ b/skills/codeman/reference/verbs.md @@ -484,7 +484,10 @@ snippet carries the exit code back to you. For a **claude** worker with no hooks, ask for the marker in halves in the prompt itself ("print the word WORKDONE immediately followed by `_`") for the same -reason, and match the joined token. ⚠️ Against a TUI, match a single space-free token: +reason, and match the joined token. For any **agent** worker without `stop` (a hook-less +claude, opencode, gemini, pi, grok, omp, copilot, a worker on the local model), the +preamble's `sendmark ""` is exactly that in one call, and also asks for the +answer in a file it reads back: [§5.16](#516-a-worker-on-the-local-model). ⚠️ Against a TUI, match a single space-free token: a full-screen TUI positions text with cursor movements rather than literal spaces, so the stripped stream can read `Yes,Itrustthisfolder`, and whether a phrase keeps its spaces depends on how the TUI happened to draw it (observed live: some match, some @@ -814,6 +817,95 @@ Rules that matter: - **Lifetime.** An inbox goes with its session on delete. A server restart empties all inboxes unless the user turned on *Keep agent inbox across restarts* in Settings. +### 5.16 A worker on the local model + +The detail behind [Claude in the cloud, a local model on this +Mac](../SKILL.md#claude-in-the-cloud-a-local-model-on-this-mac). Three preamble functions, +all read from source and pinned by `test/agent-skill-local-model.test.ts` against a fake +server; the local model routes themselves are experimental. + +**`local_model`** is one `GET /api/v1/local-model` (the `/api/v1` alias reaches the +experimental route like any other) and prints one JSON line. Exit 0 means a `local` worker +can be spawned now (`running: true`) or once `spawn_worker` has started the runtime +(`running: false`): + +```json +{"spawnable":true,"state":"installed","running":true,"cli":"opencode","cliLabel":"OpenCode", + "endpointId":"local-ollama","modelId":"qwen3.6:35b-a3b","modelLabel":"Qwen3.6 35B-A3B"} +``` + +Exit 1 carries `reason` and `message` (the server's own sentence where it has one): + +| `reason` | What it means | What to tell the user | +|----------|---------------|-----------------------| +| `not-mac` | the server is not on macOS | local models need a Mac with Apple silicon | +| `not-installed` | nothing installed; on a Mac where no model fits, `message` says why | install one with **Run a local model** on the welcome screen | +| `install-unfinished` | a job is installing, paused or failed (`state`, `step`, `errorCode`) | finish it in that window; nothing here continues it | +| `blocked` | installed, but the server refuses a session now (`errorCode`: `model-changed`, `port-in-use`, `runtime-start-failed`, `runtime-stopped`) | the window shows the one button that helps | +| `admin-only` | multi-user mode, and you are not an admin (403) | ask an admin | +| `unsupported` | the server has no local model routes (404) | it predates the feature | +| `unauthorized`, `unreachable`, `error` | the request itself failed | §6 for a 401 | + +It never downloads, installs or starts anything. Installing is many gigabytes the person +confirms in the window, and no route this skill calls can do it. + +**`spawn_worker local`** (also `name:local` in `spawn_workers`), in order: + +1. `local_model`; anything but exit 0 fails the spawn with that reason. +2. Runtime stopped: `POST /api/v1/local-model/runtime {"action":"start"}`, what the + window's Start button sends, then `local_model` again until it reports `running` + (about 30 s at most). This undoes a Stop the person made. Concurrent spawns may both + send it; the server runs one runtime verb at a time and keeps a runtime that runs. +3. `POST /api/v1/quick-start {"caseName":…,"mode":"","parentSessionId":…, + "customModel":{"endpointId":…,"modelId":…}}`: the launch of the window's "Start a + session", with the installed agent as the mode. No advisor, which is a cloud model. + The server checks again that its own runtime runs and that the model on disk is the one + it installed, and refuses otherwise (`OPERATION_FAILED`, with the reason). +4. An answer of `requiresContextWarning` or `requiresConfirmation` is a question for a + person (a context below the agent's safe floor, a model swap that would evict another + session's model). It fails the spawn with the answer on stderr and nothing on stdout, + and no `confirmed*` flag is ever sent. +5. Readiness: OpenCode's composer bar `┃` (its registry `promptGlyph`) through + `wait-output`, 45 s, and the session deleted on a miss. A claude agent takes the claude + path instead (hooks vetted, `shift+tab`, the trust dialog). Any other agent needs + `LOCAL_READY_MARK` set to a space-free token its composer draws, or the spawn is refused + before a session exists. Not measured here: an OpenCode first-run screen that hides the + composer would read as a miss. + +**`sendmark [timeoutSec]`** drives it, and any other agent worker without +an end-of-turn signal ([§5.5](#55-markers-for-hook-less-workers)): + +- It joins the prompt onto one line and adds one sentence: write the complete answer to + `codeman-answer-.md` in the working directory, and only then end the reply with + the word DONE immediately followed by `_`. The typed text never holds the joined + marker, so its echo cannot match. +- It sends once with the exactly-once body (`$CID-`, an epoch-second `seq`, one `\r`) + and a one-second `exit` wait, so `delivered` is the pane's answer, not a guess. +- It waits on `wait-output?match=DONE_&from=buffer` in rounds of at most 60 s until + the timeout (default 600 s), then reads the answer file from this machine's disk: only a + regular file (a symlink is refused), at most 256 KB. + +| exit | JSON | Meaning | +|------|------|---------| +| 0 | `answer` holds the file | the marker printed and the file was read | +| 2 | `matched: false` | no marker before the timeout. The worker may still be working: re-check the pane, never resend blindly | +| 3 | `ended: true` or `delivered: false` | the worker's pane ended | +| 4 | `matched: true, answer: null` | the marker printed, but no file within `SENDMARK_FILE_WAIT` s (default 120): the model ignored the instruction, or a reasoning model restated the marker while planning and is still at work | +| 1 | `detail` | the send or a wait failed | + +What a marker proves, and what it does not: the model printed the token. Not that the +work is done, not that it is right, and with a reasoning model not even that the turn is +over, which is why the file is the second gate. What a small local model writes is a +first draft; review it before you build on it. The answer files stay in the worker's +folder, a scratch case that [§5.14](#514-clean-up) lists for cleanup. +`SENDMARK_ANSWER_FILE=0` asks for the marker alone, for a worker that cannot write where +it runs (codex in a folder it runs read-only, an agent that asks before it writes a file, +a worker on another host): exit 0 then means only that the marker printed. A worker that +stops on such a question shows up here as exit 2, not as an error. + +Close every local worker you opened: while one is open the window refuses Stop and +Remove, naming it. + ## Experimental bounded inspection and guarded submission These three commands and their HTTP routes are labeled experimental, so they may