mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
Merge pull request #305 from Ark0N/docs/skill-hooks-rule
docs(skill): hooks are a setting now, not who created the directory
This commit is contained in:
+16
-15
@@ -47,7 +47,7 @@ later call opens with, and your first REAL call performs them anyway:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null
|
||||||
[ "${CODEMAN_PREAMBLE:-}" = 1.18.3 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
[ "${CODEMAN_PREAMBLE:-}" = 1.19.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
||||||
```
|
```
|
||||||
|
|
||||||
⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same
|
⚠️ **Never spend a Bash call on this check alone.** §1's block opens with this same
|
||||||
@@ -75,8 +75,8 @@ PRE="${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh"
|
|||||||
mkdir -p "$(dirname "$PRE")"
|
mkdir -p "$(dirname "$PRE")"
|
||||||
# Rewrite unless the file already ends with THIS version's stamp, so a stale or a
|
# Rewrite unless the file already ends with THIS version's stamp, so a stale or a
|
||||||
# half-written file self-heals here instead of costing you a round trip to rm it.
|
# half-written file self-heals here instead of costing you a round trip to rm it.
|
||||||
grep -qs '^CODEMAN_PREAMBLE=1.18.3$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
|
grep -qs '^CODEMAN_PREAMBLE=1.19.0$' "$PRE" || (umask 077; cat > "$PRE" <<'PREAMBLE'
|
||||||
# ---- Codeman agent preamble 1.18.3 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
# ---- Codeman agent preamble 1.19.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
||||||
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
||||||
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
||||||
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
||||||
@@ -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
|
||||||
@@ -233,10 +233,10 @@ last_text() {
|
|||||||
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
||||||
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
||||||
# here would fail that match and rewrite this file on every single bootstrap.
|
# here would fail that match and rewrite this file on every single bootstrap.
|
||||||
CODEMAN_PREAMBLE=1.18.3
|
CODEMAN_PREAMBLE=1.19.0
|
||||||
PREAMBLE
|
PREAMBLE
|
||||||
)
|
)
|
||||||
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.18.3 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
|
. "$PRE"; [ "${CODEMAN_PREAMBLE:-}" = 1.19.0 ] || { echo "preamble at $PRE is stale or truncated: rm it and re-run this block"; exit 1; }
|
||||||
```
|
```
|
||||||
|
|
||||||
Every later Bash call that touches the API starts with the same two loader lines from
|
Every later Bash call that touches the API starts with the same two loader lines from
|
||||||
@@ -287,7 +287,7 @@ and no per-call body to hand-build.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader
|
. "${XDG_CACHE_HOME:-$HOME/.cache}/codeman-agent-$CODEMAN_SESSION_ID.sh" 2>/dev/null # §0 loader
|
||||||
[ "${CODEMAN_PREAMBLE:-}" = 1.18.3 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
[ "${CODEMAN_PREAMBLE:-}" = 1.19.0 ] || { echo "preamble missing or stale; run the full §0 block"; exit 1; }
|
||||||
N=(alpha beta) # INVENT one fresh case name per worker; never list cases first
|
N=(alpha beta) # INVENT one fresh case name per worker; never list cases first
|
||||||
T=('reply with one line: the absolute path of your working directory'
|
T=('reply with one line: the absolute path of your working directory'
|
||||||
'reply with one line: your model name') # tasks, same order as N
|
'reply with one line: your model name') # tasks, same order as N
|
||||||
@@ -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) |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ---- Codeman agent preamble 1.18.3 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
# ---- Codeman agent preamble 1.19.0 (seeded by Codeman at session spawn; the SKILL.md §0 bootstrap rewrites it when missing or stale) ----
|
||||||
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
API="${CODEMAN_API_URL:?CODEMAN_API_URL not set; refusing to guess}"
|
||||||
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
SELF="${CODEMAN_SESSION_ID:?CODEMAN_SESSION_ID not set}"
|
||||||
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
# Credentials, cheapest first. Your session has usually INHERITED the server's
|
||||||
@@ -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
|
||||||
@@ -155,4 +155,4 @@ last_text() {
|
|||||||
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
# The stamp is the LAST line on purpose (a truncated write leaves it unset) and is kept
|
||||||
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
# bare on purpose: the write condition above anchors on it with $, so an inline comment
|
||||||
# here would fail that match and rewrite this file on every single bootstrap.
|
# here would fail that match and rewrite this file on every single bootstrap.
|
||||||
CODEMAN_PREAMBLE=1.18.3
|
CODEMAN_PREAMBLE=1.19.0
|
||||||
|
|||||||
@@ -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.19.0 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.19.0, `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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user