docs(skill): hooks are a setting now, not who created the directory

The workspace-hooks install makes the skill's central hooks rule wrong in the
cautious direction. Six places told a worker that a linked case or a raw
workingDir has no `stop`/`blocked` and that send-and-wait cannot be trusted
there, so an agent would hand-roll output-marker synchronization in exactly the
workspaces where `wait:true` now works.

Rewritten against the setting rather than directory provenance:

- verbs.md §5.1: the where-to-spawn table, the rule paragraph (now naming
  `workspaceHooksEnabled`, default ON, the add-only merge, and the boot sweep of
  recovered sessions), and the silent-failure warning. The three cases that stay
  hook-less regardless are called out: remote SSH sessions, docker cases that
  opted out, and a workspace Codeman cannot write to.
- verbs.md §5.3: the send-and-wait precondition is "the workspace has the hooks
  block", not "a case Codeman created".
- endpoints.md: the Signals-by-mode table is now keyed on the setting, with rows
  for OFF, for remote/docker-opt-out, and for a session from an older server.
  The old create-path grep list becomes a "before 1.18.x" note.
- SKILL.md §2 + the cost list, recipes.md Flow-1 contrast, messaging.md step 1.

"Check, do not assume" is kept and promoted to the load-bearing habit, because
the setting is not visible from the call and a session created by an older server
that has not restarted still has nothing.

The `spawn_worker` hooks grep STAYS: it guards the setting being off, remote
sessions, and older servers. Only its diagnostic changes, since "pick an unused
name" is no longer the fix. That text lives in both the §0 heredoc and
`preamble.sh`, which `test/agent-skill.test.ts` pins byte-identical, so both are
patched with the same bytes.

Docs only, no behavior change. 23 skill tests green, full test:ci 5109 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-16 07:30:10 +02:00
parent f485085174
commit 1c94995290
6 changed files with 91 additions and 79 deletions
+10 -9
View File
@@ -138,14 +138,14 @@ spawn_worker() {
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1). # NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; } [ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
[ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # only claude draws a composer [ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # only claude draws a composer
# quick-start RESOLVES the name before creating: a linked case or an existing dir # The server installs hooks into every claude workspace now, so this grep normally
# wins over a fresh scratch case, so "created => hooks" is only true after this one # passes; it stays because the install is gated on a setting the operator can turn
# local grep (the same marker the server itself checks for). No marker means sendwait # off, remote sessions never get hooks, and a session created by an older server
# would false-resolve on flapping idle, possibly inside the user's REAL repo: refuse # still has none. No marker means sendwait would false-resolve on flapping idle,
# rather than run the job there. # possibly inside the user's REAL repo: refuse rather than run the job there.
cp=$(jq -r '.data.casePath // empty' <<<"$q") cp=$(jq -r '.data.casePath // empty' <<<"$q")
grep -qs '/api/hook-event' "$cp/.claude/settings.local.json" || { grep -qs '/api/hook-event' "$cp/.claude/settings.local.json" || {
echo "case '$name' resolved to '$cp', which has no Codeman hooks (linked or pre-existing?): pick an unused name, or work §5.1+§5.5 by hand" >&2 echo "case '$name' resolved to '$cp', which has no Codeman hooks (workspaceHooksEnabled off, remote, or an older server?): turn the setting on, or work §5.1+§5.5 by hand with markers" >&2
delete_session "$sid" >/dev/null; return 1; } delete_session "$sid" >/dev/null; return 1; }
# Short composer wait FIRST, then the trust-dialog probe: a case still showing the # Short composer wait FIRST, then the trust-dialog probe: a case still showing the
# dialog can never pass the composer wait, so probing early keeps a cold case from # dialog can never pass the composer wait, so probing early keeps a cold case from
@@ -343,7 +343,8 @@ Four things this block leans on, each one link away, no detour needed to run it:
`~/codeman-cases/<name>`, not your repo. A name that already means something (a `~/codeman-cases/<name>`, not your repo. A name that already means something (a
linked case, a pre-existing directory) is refused by `spawn_worker` rather than linked case, a pre-existing directory) is refused by `spawn_worker` rather than
silently reused. Spawning where the work actually is (a linked case, a git worktree) silently reused. Spawning where the work actually is (a linked case, a git worktree)
is a different call with **no hooks**, and the costliest mistake in this skill: §5.1. is a different call, and picking the wrong one is the costliest mistake in this
skill: §5.1. Those workspaces do get hooks now, unless the operator disabled it.
- `sendwait` supplies the `\r`, picks a fresh `seq`, and self-heals a stranded Enter. - `sendwait` supplies the `\r`, picks a fresh `seq`, and self-heals a stranded Enter.
A prompt without the `\r` is never submitted (§3), a reused `seq` is silently A prompt without the `\r` is never submitted (§3), a reused `seq` is silently
swallowed as an already-applied duplicate, and an Enter eaten by an Ink repaint swallowed as an already-applied duplicate, and an Enter eaten by an Ink repaint
@@ -358,9 +359,9 @@ One row per job. Acting on this table alone is correct; the §5 links are the de
| I want to | Call | Detail | | I want to | Call | Detail |
|-----------|------|--------| |-----------|------|--------|
| 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: full signals there. Any other path (a git worktree): `POST /api/v1/sessions {"workingDir":…}` then `POST /api/v1/sessions/:id/interactive`, and expect **no hooks**. N workers means N worktrees | [§5.1](reference/verbs.md#51-where-to-spawn) | | 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) |
| 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) | | 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) |
| deliver a task **and** know when it finished | `POST .../input` with `"input":"…\r"`, `clientId`, `seq`, `"wait":true`. Resolves on `stop`, so it is only trustworthy in a **case Codeman created** (claude mode + hooks present). Costs the worker one billed turn | [§5.3](reference/verbs.md#53-send-a-task-and-wait) | | deliver a task **and** know when it finished | `POST .../input` with `"input":"…\r"`, `clientId`, `seq`, `"wait":true`. Resolves on `stop`, so it is trustworthy only where the workspace **has hooks** (claude mode; installed by default, but the operator can disable it and remote sessions never get them). Costs the worker one billed turn | [§5.3](reference/verbs.md#53-send-a-task-and-wait) |
| 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 | [§5.5](reference/verbs.md#55-markers-for-hook-less-workers) |
| read the answer | `GET .../last-response`, **polled** (claude/codex only; empty for the other modes) | [§5.4](reference/verbs.md#54-read-the-answer) | | read the answer | `GET .../last-response`, **polled** (claude/codex only; 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 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) |
+6 -6
View File
@@ -60,14 +60,14 @@ spawn_worker() {
# NOT retryable in a loop: every quick-start failure code is terminal (§5.1). # NOT retryable in a loop: every quick-start failure code is terminal (§5.1).
[ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; } [ -n "$sid" ] || { jq -c '{error,errorCode}' <<<"$q" >&2; return 1; }
[ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # only claude draws a composer [ "$mode" = claude ] || { printf '%s\n' "$sid"; return 0; } # only claude draws a composer
# quick-start RESOLVES the name before creating: a linked case or an existing dir # The server installs hooks into every claude workspace now, so this grep normally
# wins over a fresh scratch case, so "created => hooks" is only true after this one # passes; it stays because the install is gated on a setting the operator can turn
# local grep (the same marker the server itself checks for). No marker means sendwait # off, remote sessions never get hooks, and a session created by an older server
# would false-resolve on flapping idle, possibly inside the user's REAL repo: refuse # still has none. No marker means sendwait would false-resolve on flapping idle,
# rather than run the job there. # possibly inside the user's REAL repo: refuse rather than run the job there.
cp=$(jq -r '.data.casePath // empty' <<<"$q") cp=$(jq -r '.data.casePath // empty' <<<"$q")
grep -qs '/api/hook-event' "$cp/.claude/settings.local.json" || { grep -qs '/api/hook-event' "$cp/.claude/settings.local.json" || {
echo "case '$name' resolved to '$cp', which has no Codeman hooks (linked or pre-existing?): pick an unused name, or work §5.1+§5.5 by hand" >&2 echo "case '$name' resolved to '$cp', which has no Codeman hooks (workspaceHooksEnabled off, remote, or an older server?): turn the setting on, or work §5.1+§5.5 by hand with markers" >&2
delete_session "$sid" >/dev/null; return 1; } delete_session "$sid" >/dev/null; return 1; }
# Short composer wait FIRST, then the trust-dialog probe: a case still showing the # Short composer wait FIRST, then the trust-dialog probe: a case still showing the
# dialog can never pass the composer wait, so probing early keeps a cold case from # dialog can never pass the composer wait, so probing early keeps a cold case from
+26 -32
View File
@@ -251,10 +251,12 @@ that is expected, not a failure: read `terminal?tail=` and strip ANSI instead.
**It means** that session has no Codeman hooks, so `stop` can never fire and the wait **It means** that session has no Codeman hooks, so `stop` can never fire and the wait
silently degraded to `idle`, which flaps mid-turn. Nothing rejected your request: silently degraded to `idle`, which flaps mid-turn. Nothing rejected your request:
`wait:true` (and even an explicit `until=stop`) is accepted because the 400 is about `wait:true` (and even an explicit `until=stop`) is accepted because the 400 is about
session **mode**, and the mode really is `claude`. Hooks are written only when Codeman session **mode**, and the mode really is `claude`. Hooks are installed into every
**creates** the directory; a linked case or a raw `workingDir` gets none (an existing claude workspace at session create (synced `workspaceHooksEnabled`, default ON) and
case that Codeman created earlier keeps the block it was given), see the table under swept across recovered sessions at boot, so a linked case or a raw `workingDir` gets
[Signals by mode](#signals-by-mode). Measured: on a them too; with the setting off, on a remote session, or on a session from an older
server, they are absent, see the table under
[Signals by mode](#signals-by-mode). Measured before that changed: on a
linked case whose `.claude/settings.local.json` carries env/model/permissions/statusLine linked case whose `.claude/settings.local.json` carries env/model/permissions/statusLine
and no `hooks` block, a `wait?until=stop,exit` parked for twelve consecutive 60 s rounds and no `hooks` block, a `wait?until=stop,exit` parked for twelve consecutive 60 s rounds
never resolved although the worker finished its turn. never resolved although the worker finished its turn.
@@ -360,10 +362,10 @@ loop.
⚠️ `caseName` resolves through the linked-cases registry first, so a name that happens ⚠️ `caseName` resolves through the linked-cases registry first, so a name that happens
to match a case the user linked in lands in that **real repo**, not a fresh scratch to match a case the user linked in lands in that **real repo**, not a fresh scratch
directory. Pick distinctive scratch names, and use a linked name deliberately when you directory. Pick distinctive scratch names, and use a linked name deliberately when you
do want a worker in an existing checkout. ⚠️ It also decides whether you get hooks: do want a worker in an existing checkout. It no longer decides whether you get hooks:
Codeman writes them only when it **creates** the directory, so a linked case or a raw every claude create path installs them, so a linked case and a raw path both get a
path gives you a worker with no `stop` signal, while a scratch case Codeman created `stop` signal unless the operator turned `workspaceHooksEnabled` off
earlier keeps working signals ([Signals by mode](#signals-by-mode)). ([Signals by mode](#signals-by-mode)).
**The two-step alternative, `POST /api/v1/sessions`.** Use it when you need a session in **The two-step alternative, `POST /api/v1/sessions`.** Use it when you need a session in
a directory that is not a case (body takes `workingDir`, `mode`, `name`, `effort`, a directory that is not a case (body takes `workingDir`, `mode`, `name`, `effort`,
@@ -614,35 +616,27 @@ Three bounded long-polls. Shared semantics:
| `exit` | PTY exited or session deleted | every mode | | `exit` | PTY exited or session deleted | every mode |
⚠️ **`claude` mode is necessary for `stop`/`blocked`, not sufficient. The real ⚠️ **`claude` mode is necessary for `stop`/`blocked`, not sufficient. The real
precondition is that the session's working directory has a Codeman hooks block**, and precondition is that the session's working directory has a Codeman hooks block**, which
whether it does depends on who created the directory: is now installed by default rather than depending on who created the directory:
| The worker's directory | Hooks | `stop` / `blocked` | Synchronize with | | The worker's directory | Hooks | `stop` / `blocked` | Synchronize with |
|------------------------|-------|--------------------|------------------| |------------------------|-------|--------------------|------------------|
| Codeman created it (`quick-start` with a NEW `caseName`, `POST /api/cases`, clone, docker quickcreate) | written at create | fire | send-and-wait on `stop` | | any claude workspace, with `workspaceHooksEnabled` ON (the default) | installed at session create, add-only merge | fire | send-and-wait on `stop` |
| Codeman never created it (a linked case pointing at your own checkout, a raw `workingDir`) | none written | never fire | `wait-output` markers only | | the same, with the setting OFF and no block already on disk | none added | never fire | `wait-output` markers only |
| a remote SSH session, a docker case that opted out, a workspace Codeman cannot write | none | never fire | `wait-output` markers only |
| a session created by a pre-1.18.x server and never restarted since | whatever it had | only if present | check, then choose |
⚠️ **Docker cases are the one exception.** For a docker case, quick-start writes hooks The install is an add-only merge, so a user's own hook entries survive and a malformed
whenever `.claude/settings.local.json` is *missing* (`session-routes.ts:2836-2845`: settings file is left untouched. Sessions recovered at server boot get the same sweep,
absent means write, present means refresh), regardless of who created that host which is what heals sessions created before this behavior existed. When in doubt, test
directory. There the discriminator really is "does the settings file exist". No it rather than reason about it: grep for `/api/hook-event` in
downstream advice changes, since docker quickcreate is already on the create side. `<casePath>/.claude/settings.local.json`.
⚠️ For every non-docker case the discriminator is **who created the directory, not Before 1.18.x, `writeHooksConfig()` ran only on the create paths and `quick-start`
whether it exists now**. A against an existing directory called `refreshStaleCodemanHooks()`, which never *adds* a
scratch case Codeman created last week still has its hooks block on disk, so block, so a linked case or a raw `workingDir` had no hooks at all. `POST
`quick-start` against that existing name gets working `stop` signals. Only a directory /api/cases/link` still only records a name-to-path entry; what changed is that the
Codeman never created lacks them. When in doubt, test it rather than reason about it: session-create path installs hooks regardless of how the directory got there. See
grep for `/api/hook-event` in `<casePath>/.claude/settings.local.json`.
`writeHooksConfig()` runs only on the create paths (`case-routes.ts:341`, `:520`,
`:869`, `ralph-routes.ts:318`, `session-routes.ts:2799` inside
`if (!existsSync(resolvedCasePath))`, `:2841` for docker). Quick-start against a
directory that already exists takes the else-if branch and calls
`refreshStaleCodemanHooks()`, which returns immediately when there is no
`settings.local.json` and again when the hooks it finds are not ours
(`hooks-config.ts:706-731`); it never *adds* a hooks block. `POST /api/cases/link` is
not on that list at all: it only records a name-to-path entry. See
[symptom 8](#8-send-and-wait-resolves-instantly-with-signalidle-and-the-answer-is-last-turns). [symptom 8](#8-send-and-wait-resolves-instantly-with-signalidle-and-the-answer-is-last-turns).
Default `until` set: `stop,idle,exit`. On non-claude modes the server silently drops Default `until` set: `stop,idle,exit`. On non-claude modes the server silently drops
+8 -6
View File
@@ -196,12 +196,14 @@ idle:
The contract an orchestrator follows for any fleet of two or more messaging workers. The contract an orchestrator follows for any fleet of two or more messaging workers.
Every topology in the next section is this protocol plus a wiring diagram. Every topology in the next section is this protocol plus a wiring diagram.
1. **Spawn with a name, and with hooks.** Use `quick-start` with `sessionName` (the 1. **Spawn with a name, and confirm hooks.** Use `quick-start` with `sessionName` (the
`--name` gate above), and let it **CREATE** the case. ⚠️ Linking does NOT install `--name` gate above). Session create installs the hooks block into the workspace
hooks (`POST /api/cases/link` writes only the name-to-path entry), and neither does a whatever kind it is, so a linked case and a raw `POST /api/sessions` path both get
bare `POST /api/sessions`; a worker in a directory Codeman did not create has no `stop`/`blocked` by default. ⚠️ Not unconditionally: the operator can turn
`stop`/`blocked` signals at all and every synchronization below degrades to output `workspaceHooksEnabled` off, remote SSH sessions never get hooks, and a session from
markers. The discriminator is who created the directory, not whether it exists now. an older server may have none, and without them every synchronization below degrades
to output markers. Grep `<casePath>/.claude/settings.local.json` for
`/api/hook-event` at spawn rather than inferring it from how the directory got there.
2. **Readiness before addressing.** Flow 1's ladder per worker, then the availability 2. **Readiness before addressing.** Flow 1's ladder per worker, then the availability
probe. A worker that fails the probe is an HTTP worker for the rest of the run; that probe. A worker that fails the probe is an HTTP worker for the rest of the run; that
is a routing decision, not an error. is a routing decision, not an error.
+3 -3
View File
@@ -492,9 +492,9 @@ What breaks if you use send-and-wait anyway: `wait:true` is accepted (the 400 is
*mode*, not about hooks, and these are claude-mode sessions), so the call falls back to *mode*, not about hooks, and these are claude-mode sessions), so the call falls back to
the default set's `idle`, which is a heuristic that flaps mid-turn. You get a "finished" the default set's `idle`, which is a heuristic that flaps mid-turn. You get a "finished"
answer for a turn still running, and `last-response` then hands you the *previous* answer for a turn still running, and `last-response` then hands you the *previous*
turn's text. The contrast is the lesson: a worker in a case Codeman created (Flow 1) has turn's text. The contrast is the lesson: a worker whose workspace carries the hooks
the hooks, so `stop` there is definitive and free. In a worktree you pay one marker per block (Flow 1, and by default any other workspace too) has a `stop` that is definitive
worker instead. and free. Where the block is absent you pay one marker per worker instead.
```bash ```bash
declare -A TOK declare -A TOK
+38 -23
View File
@@ -30,28 +30,40 @@ wrong directory.** `quick-start` with a new `caseName` does not find your repo:
| Where the work is | Call | Hooks, and therefore signals | | Where the work is | Call | Hooks, and therefore signals |
|-------------------|------|------------------------------| |-------------------|------|------------------------------|
| a fresh scratch dir (throwaway experiments) | `POST /api/v1/quick-start {"caseName":"scratch-1","mode":"claude"}` with a **new** case name | Codeman creates the directory and **writes hooks**: `stop` and `blocked` fire, send-and-wait is trustworthy | | a fresh scratch dir (throwaway experiments) | `POST /api/v1/quick-start {"caseName":"scratch-1","mode":"claude"}` with a **new** case name | Codeman creates the directory and **writes hooks**: `stop` and `blocked` fire, send-and-wait is trustworthy |
| a linked case (a real repo in the linked-cases registry) | same call with the linked name | **no hooks**, unless that repo already carries a Codeman hooks block from some earlier path. Check before relying on `stop` | | a linked case (a real repo in the linked-cases registry) | same call with the linked name | **hooks installed at session create**, so `stop` fires here too. Not guaranteed: the operator can turn it off. Check |
| any other absolute path, e.g. a git worktree you made | `POST /api/v1/sessions {"workingDir":"/abs/path","mode":"claude"}` then `POST /api/v1/sessions/:id/interactive` | **no hooks**: no `stop`, no `blocked`, synchronize with markers ([§5.5](#55-markers-for-hook-less-workers)) | | any other absolute path, e.g. a git worktree you made | `POST /api/v1/sessions {"workingDir":"/abs/path","mode":"claude"}` then `POST /api/v1/sessions/:id/interactive` | same: **hooks installed at session create**, subject to the same setting. Check |
Read `.data.casePath` back from the `quick-start` response and check it is where you Read `.data.casePath` back from the `quick-start` response and check it is where you
meant. `caseName` accepts letters, digits, `-` and `_` only, and it resolves through meant. `caseName` accepts letters, digits, `-` and `_` only, and it resolves through
the linked-cases registry **first**, so a name that collides with something the user the linked-cases registry **first**, so a name that collides with something the user
linked in lands in that real repo rather than a scratch dir. linked in lands in that real repo rather than a scratch dir.
**The rule is who created the directory.** Codeman writes hooks only where it created **The rule is a setting, not who created the directory.** Every claude create path
the workspace itself: `quick-start` on a NEW case name, `POST /api/cases`, the repo (`POST /api/sessions`, `POST /api/quick-start`, and quick-start's docker branch) now
clone, the docker quick-create. Those hooks persist, so a scratch case created last installs the hooks block into the workspace, and the server sweeps the workspaces of
week still has them today. A directory that already existed when Codeman first pointed sessions it recovers at boot. So a linked case, a cloned repo and a hand-made git
at it never gets them: `POST /api/cases/link` writes only the name-to-path entry in worktree all get `stop`/`blocked`, not just a scratch case Codeman scaffolded. The
`linked-cases.json`, and quick-start into an existing path runs install is an **add-only merge**: a user's own hook entries and every other settings
`refreshStaleCodemanHooks()`, which by design returns immediately when there is no key survive, and a malformed settings file is left alone.
Codeman hooks block to refresh. Source-verified by exhaustive call-site grep, and
measured: a worker in a linked case never resolved a parked `wait?until=stop,exit`
across twelve consecutive 60 s rounds, although it had finished its turn.
**Check, do not assume.** Read `<casePath>/.claude/settings.local.json` with your own The gate is the synced **`workspaceHooksEnabled`** setting, **default ON** (an absent
file tools and look for `/api/hook-event`. Present means `stop`/`blocked` will fire; key counts as ON). Turned OFF, the old behavior returns exactly: an existing Codeman
absent means they never will. block is still refreshed when stale, but one is never added, and the boot sweep is
skipped. Three cases stay hook-less regardless: **remote SSH sessions** (their
`workingDir` is a path on another host), **docker cases that opted out**, and any
workspace Codeman cannot write to.
Until this landed, hooks existed only where Codeman created the directory, and the
gap was invisible: a worker in a linked case never resolved a parked
`wait?until=stop,exit` across twelve consecutive 60 s rounds, although it had finished
its turn. If you are driving an older server, assume that older rule.
**Check, do not assume.** This is now the load-bearing habit, because you cannot tell
from the call which way the setting is set, and an old session created before the fix
on a server that has not restarted still has nothing. Read
`<casePath>/.claude/settings.local.json` with your own file tools and look for
`/api/hook-event`. Present means `stop`/`blocked` will fire; absent means they never
will, whatever kind of workspace it is.
⚠️ **The hook-less failure is silent, and it is the worst one in this skill.** ⚠️ **The hook-less failure is silent, and it is the worst one in this skill.**
`"wait":true` is still **accepted** on a hook-less claude session: the 400 you may be `"wait":true` is still **accepted** on a hook-less claude session: the 400 you may be
@@ -59,9 +71,10 @@ expecting is about session *mode*, not about hooks. With no `stop` to resolve on
default signal set falls back to the heuristic `idle`, which flaps mid-turn, so default signal set falls back to the heuristic `idle`, which flaps mid-turn, so
send-and-wait returns "finished" while the worker is still working, and the send-and-wait returns "finished" while the worker is still working, and the
`last-response` you read next hands you the **previous** turn's text. No error is `last-response` you read next hands you the **previous** turn's text. No error is
raised anywhere. In any workspace Codeman did not create, use markers raised anywhere. Hooks are installed by default now, so this is rarer than it was, but
([§5.5](#55-markers-for-hook-less-workers)) and treat send-and-wait's answer as the failure is unchanged when it happens: in any workspace whose settings file has no
unreliable. `/api/hook-event`, use markers ([§5.5](#55-markers-for-hook-less-workers)) and treat
send-and-wait's answer as unreliable.
Spawning at a raw path: Spawning at a raw path:
@@ -238,11 +251,13 @@ fi
### 5.3 Send a task and wait ### 5.3 Send a task and wait
⚠️ **Precondition: this is the call to prefer only for a claude worker in a workspace ⚠️ **Precondition: a claude worker whose workspace has the hooks block**, because
Codeman created**, because it is trustworthy only when the `stop` hook exists. On a this is trustworthy only when the `stop` hook exists. Every claude create path installs
linked case or a raw path it is accepted, resolves on flapping `idle`, and reports a it by default now, so that is the normal case, but where it is absent (the setting off,
turn as finished while it is still running, with no error anywhere. Check hooks first a remote session, an older server) the call is still accepted, resolves on flapping
([§5.1](#51-where-to-spawn)); where they are absent, use markers `idle`, and reports a turn as finished while it is still running, with no error
anywhere. Check hooks first ([§5.1](#51-where-to-spawn)); where they are absent, use
markers
([§5.5](#55-markers-for-hook-less-workers)). ([§5.5](#55-markers-for-hook-less-workers)).
It registers the waiter *before* typing, It registers the waiter *before* typing,