` or `--on-finish CMD`. The hook fires exactly once per run on any terminal state, with the result on stdin and `DK_RUN_ID`, `DK_STATUS`, `DK_RESULT_PATH` in the environment; a non-zero exit is retried, delivery survives a dead supervisor. Point it where the coordinator will actually look — a log it tails, a desktop notifier. Without a hook, an external worker is invisible until polled.
+- **Run states.** `status` is how a run ended; `lifecycle` is whether `resume` can still revive it (`parked`) or not (`done`). `orphaned` (supervisor died) and `timeout` are terminal but the worker has usually committed before dying — read the worktree before rerunning.
+- **Timeouts are a fuse.** Writers and planners 90 min, reviewers and researchers 45; `--timeout` raises one run. Set it up front for a brief you expect to be long.
+
+### Under a Codex coordinator
+
+Codex's shell tool yields after `yield_time_ms` (default 1 s) and returns `Process running with session ID N` — the process is alive, the output is simply not there yet. A blocking `agent-run run` or `wait` therefore needs `yield_time_ms: 300000` on every call, and an early return is reaped by session id, never retried with a fresh `run` (each retry starts one more worker). `--detach` + `status` polling sidesteps the issue.
+
+## Presets: which subscription pays
+
+Quota is not symmetric over time. A preset moves the token-heavy roles — planner, implementer, researcher — onto one family:
+
+| Preset | planner | implementer | researcher | reviewer / verifier |
+|---|---|---|---|---|
+| `auto` (default) | claude | codex | claude | derived from the author |
+| `main-claude` | claude | claude | claude | derived from the author |
+| `main-codex` | codex | codex | codex | derived from the author |
+
+The reviewer follows the **author**, never the preset: `main-claude` implies a Codex reviewer, `main-codex` a Claude one. A review is one read-only pass over a frozen diff, a fraction of what the implementer spends, so the preset still moves the bulk of the cost. Under `auto`, `--kind ui` sends the implementation to Claude and the reviewer follows.
+
+Precedence: the words in the request (`main-claude`, `main-codex`; aliases `main-gpt`/`main-openai`, `main-anthropic`) → `--preset` per call → `DELEGATE_KIT_PRESET` → `agent-run preset ` persisted in `~/.delegate-kit/config.json` → `auto`. Explicit `--backend` / `--model` / `--effort` win over the preset, and the user can name a model for one role ("review with Sol").
+
+Per-role defaults for one user, in the same file — the shipped table in `scripts/agent-run` stays the default for everyone:
+
+```json
+{ "preset": "main-claude",
+ "roles": { "planner": { "claude": ["fable", "xhigh"] }, "reviewer": { "codex": ["gpt-5.6-sol", "xhigh"] } } }
+```
+
+`agent-run preset` alone prints the effective table. A malformed entry is reported and ignored.
+
+## Limits and safety
+
+- **Delegation depth is 1.** `agent-run` disables subagents on both CLIs; the shipped `dk-*` definitions carry no `Agent` tool.
+- **Writers** run in a worktree under the backend's own sandbox (`workspace-write` / `acceptEdits`); the dangerous modes are outside this skill. `agent-run` refuses a worktree locked for a native writer, and the reverse.
+- **Read-only roles** run under `codex -s read-only` / `claude --permission-mode plan` — the enforced boundary a native role lacks. When it matters (an untrusted diff, a risk zone), dispatch that role externally even inside the family.
+- **Quota fallback.** On a usage or rate limit `agent-run` retries the brief once on the other family and marks the result `fallback_from`. For a reviewer that can land the review on the author's family — the result says so; report it or re-run later. `--fallback none` disables it; resumes never fall back.
+- `hooks/gate.sh` makes dangerous shell commands need the user's confirmation in the coordinator (Claude: the approval prompt; Codex: denied with instructions to confirm and re-run prefixed `DELEGATE_KIT_CONFIRMED=1`).
+- The ledger `~/.delegate-kit/ledger.jsonl` records model, effort, preset, lens, tokens, duration and outcome per external run. Read it before changing a default.
+
+A Claude coordinator under `main-claude` runs everything natively and pays for exactly one external session — the Codex reviewer. That is the cheapest shape this skill has.
diff --git a/skills/delegate-kit/references/hosts.md b/skills/delegate-kit/references/hosts.md
new file mode 100644
index 0000000..dfa1917
--- /dev/null
+++ b/skills/delegate-kit/references/hosts.md
@@ -0,0 +1,27 @@
+# Hosts: how a native worker is dispatched
+
+A native worker is a subagent of the host, in the coordinator's own family. The policy is the same everywhere; the fan-out verb differs. A worker from the other family is never native — `external.md`.
+
+| Host | Native fan-out | Role definition | Isolation for a writer | Completion |
+|---|---|---|---|---|
+| Claude Code, T3 Code | `Agent` tool, `subagent_type: dk-`; several in one message run in parallel; `run_in_background` for long ones | `~/.claude/agents/dk-*.md` (symlinked by `hooks/install.sh`) | `agent-wt create` + `agent-wt lock`, path in the brief; or the tool's own `isolation: worktree` when the branch does not need `agent-wt diff` | task notification; `SendMessage` continues the same subagent |
+| Codex CLI | `spawn_agent`, agent `dk-` | `[agents.dk-*]` in `~/.codex/config.toml` (spliced by `hooks/install.sh`) | `agent-wt create` + `agent-wt lock` | the tool result; Codex asks the user before spawning unless a standing grant exists |
+| Pi, OpenCode, other | the host's dispatch tool if one exists | the brief carries the role preamble from `roles.md` | `agent-wt create` + `agent-wt lock` | the tool result |
+| No fan-out at all | **serial**: the coordinator runs one worker after another, or does the slice itself | — | `agent-wt` still applies | — |
+
+Serial is a dispatch mode, not a failure: one long-lived worker resumed with each next slice beats a queue of orphaned parallel ones. External review still works on any host that can run a shell.
+
+## Native gives up three things
+
+Choose external even inside the family when one matters:
+
+- **An enforced sandbox.** External read-only roles run under `claude --permission-mode plan` / `codex -s read-only`. A native role is read-only by instruction and tool list — fine for a reviewer you dispatched yourself, thin as the boundary around an untrusted diff.
+- **The strict result schema, ledger, run id, timeout, quota fallback.** All live in `agent-run`.
+- **The write-lock for free.** `agent-run --cwd` takes it; a native writer needs `agent-wt lock` before and `agent-wt release` after.
+
+## Git with parallel writers
+
+- Each writer commits only its own paths; `git add `, never `git add -A`.
+- On `index.lock`, find the owner (`lsof`, `ps`) before touching it: live owner → wait; none → stale, remove.
+- `git log --oneline -20` in each worktree shows a stalled or off-pattern worker sooner than its report does.
+- Push is the coordinator's call after integration, never the worker's.
diff --git a/skills/delegate-kit/references/review.md b/skills/delegate-kit/references/review.md
index 9a5dc76..a9ab01d 100644
--- a/skills/delegate-kit/references/review.md
+++ b/skills/delegate-kit/references/review.md
@@ -4,7 +4,7 @@ How many reviewers a diff deserves, which angle each one takes, and how their fi
## Independence is the first slot, not the whole review
-One reviewer from the other family than the author buys **independence**: two families share fewer blind spots than one. That is the invariant of this skill and nothing moves it — not a preset, not a panel, not quota.
+One reviewer from the other family than the author buys **independence**: two families share fewer blind spots than one. No preset, panel or quota pressure moves slot A off the other family. The one thing that does is availability: when that family's CLI is not installed, `route` places slot A as a fresh read-only worker of the author's family, marks it `independent: false`, and the report names which one ran. A fresh context is still a real review; the other family is the stronger one.
A second reviewer with the same brief buys almost nothing: the obvious findings come back twice and the subtle ones stay missed, because both reviewers looked from the same angle. What a second slot should buy is a second **lens**. So a panel is composed as lenses first, families second:
diff --git a/skills/delegate-kit/references/roles.md b/skills/delegate-kit/references/roles.md
index 8352aa8..8cf88fe 100644
--- a/skills/delegate-kit/references/roles.md
+++ b/skills/delegate-kit/references/roles.md
@@ -1,6 +1,6 @@
# Roles: who, why, and how to prompt them
-`agent-run route --role ` prints the default for any role; this file is the reasoning, so you can deviate deliberately. Where a worker runs and which subscription pays are in `dispatch.md`; review depth and lenses in `review.md`.
+`agent-run route --role ` prints the default for any role; this file is the reasoning, so you can deviate deliberately. Where a worker runs and which subscription pays are in `external.md`; review depth and lenses in `review.md`.
## The two families
@@ -21,7 +21,7 @@ The Codex CLI is offered `sol`, `terra` and `luna` only (`~/.codex/models_cache.
|---|---|---|---|
| planner | `fable` high / `gpt-5.6-sol` xhigh | preset | read-only |
| implementer | `opus` high / `gpt-5.6-sol` high | preset; `--kind ui` → Claude under `auto` | write, in a worktree |
-| reviewer | `opus` high / `gpt-5.6-sol` high | **the other family than the author** | read-only |
+| reviewer | `opus` high / `gpt-5.6-sol` high | **the other family than the author** when its CLI is installed; else a fresh worker of the author's family, reported | read-only |
| review-lead | `fable` high / `gpt-5.6-sol` xhigh | the planner's family | read-only |
| verifier | `fable` high / `gpt-5.6-sol` xhigh | third party to the reviewer | read-only |
| researcher | `sonnet` medium / `gpt-5.6-terra` medium | preset | read-only, web |
@@ -51,7 +51,7 @@ The Codex CLI is offered `sol`, `terra` and `luna` only (`~/.codex/models_cache.
## reviewer
- **Default** the other family than the author, high effort. After a Codex implementer: `claude` opus high. After a Claude implementer: `codex` gpt-5.6-sol high. UX/product review: `claude` opus.
-- **Why the other family**: the same family reviewing itself shares blind spots. Independence is the value — and it is the first slot of a panel, never something a preset moves.
+- **Why the other family**: the same family reviewing itself shares blind spots. Independence is the value — the first slot of a panel, never something a preset moves. When the other CLI is missing, `route` falls back to a fresh native reviewer and says so; a fresh context is the floor, the other family the preference.
- **Depth and lenses**: one reviewer by default; a panel of two or three lenses for large or risky diffs, proposed with numbers and run only on the user's yes. `review.md`.
- **Read-only.** Give it the frozen diff (`agent-wt diff`) and the spec. Ask for findings with severity, file:line, the claim, the evidence, and a suggested fix. Ask it to separate "spec mismatch" from "standards" from "nit". Ask it not to restate the diff.
- **Effort**: high by default; xhigh only in risk zones (auth, payments, migrations).
@@ -88,7 +88,7 @@ Almost never as workers, though. The start-up cost dominates — a worker that r
- Routine orchestration: Opus 5 / Sol / Sonnet 5 / Terra. The parent writes briefs and reads reports; that does not need the top model.
- Switch the parent up (Fable / Sol xhigh) for a grill session, an architecture decision, or a hard bug the parent must reason about itself. Switch back afterwards.
-- The parent's family decides which roles can be native at all (`dispatch.md`).
+- The parent's family decides which roles can be native at all (`external.md`, `hosts.md`).
## Tuning
diff --git a/skills/delegate-kit/scripts/agent-run b/skills/delegate-kit/scripts/agent-run
index 4bc5235..2a2b7fa 100755
--- a/skills/delegate-kit/scripts/agent-run
+++ b/skills/delegate-kit/scripts/agent-run
@@ -136,6 +136,7 @@ function reconcile(m) {
return m;
}
const sh = (cmd, args, opts = {}) => spawnSync(cmd, args, { encoding: "utf8", ...opts });
+const cliAvailable = (backend) => sh("/bin/sh", ["-c", `command -v ${backend === "codex" ? "codex" : "claude"}`]).status === 0;
function parseArgs(argv) {
const out = { _: [] };
@@ -489,16 +490,34 @@ function resolveRoute(opts = {}) {
backend = opts.backend;
}
- const [defModel, defEffort] = ROLES[role][backend];
- const model = opts.model && opts.model !== true ? opts.model : defModel;
- const effort = opts.effort && opts.effort !== true ? opts.effort : defEffort;
const write = ROLES[role].write;
- const dispatch = parent && parent === backend ? "native" : "external";
- why.push(dispatch === "native"
- ? `the parent (${parent}) is in the same family, so it can spawn this worker natively`
- : parent
+ let dispatch = parent && parent === backend ? "native" : "external";
+ // Independence prefers the other family, but is not gated on it: when that family's CLI
+ // is not installed, the reviewer/verifier lands on the parent's own family as a fresh,
+ // read-only native worker, and `independence` records which one the caller got.
+ let independence = ["reviewer", "verifier"].includes(role) ? (backend === author ? "same-family" : "other-family") : undefined;
+ let fellBack = false;
+ if (dispatch === "external" && !cliAvailable(backend)) {
+ const userPinned = opts.backend && opts.backend !== true && !opts._composed;
+ if (parent && ["reviewer", "verifier"].includes(role) && !userPinned) {
+ why.push(`the ${backend} CLI is not installed, so the ${role} falls back to a fresh native ${parent} worker — say so in the report`);
+ backend = parent; dispatch = "native"; fellBack = true;
+ independence = backend === author ? "same-family" : "other-family";
+ } else {
+ why.push(`the ${backend} CLI is not installed; install it or pass --backend ${otherBackend(backend)}`);
+ }
+ }
+ if (dispatch === "native") {
+ if (!fellBack) why.push(`the parent (${parent}) is in the same family, so it can spawn this worker natively`);
+ } else {
+ why.push(parent
? `the parent (${parent}) cannot spawn a ${backend} worker natively, so it goes through agent-run`
: "no parent harness detected (pass --parent), so external dispatch is assumed");
+ }
+
+ const [defModel, defEffort] = ROLES[role][backend];
+ const model = opts.model && opts.model !== true ? opts.model : defModel;
+ const effort = opts.effort && opts.effort !== true ? opts.effort : defEffort;
const external = ["agent-run run", `--role ${role}`, `--backend ${backend}`, `--model ${model}`, `--effort ${effort}`,
write ? "--cwd " : "", "--brief "].filter(Boolean).join(" ");
@@ -515,7 +534,7 @@ function resolveRoute(opts = {}) {
? "take the lock yourself: `agent-wt create ` then `agent-wt lock --label dk-implementer`, name the worktree path in the prompt, and `agent-wt release ` when the subagent returns"
: "role definition lives in ~/.codex/config.toml under [agents.dk-*]; install it with hooks/install.sh" };
- return { role, preset, parent: parent ?? "unknown", backend, model, effort, write, dispatch, invoke, why,
+ return { role, preset, parent: parent ?? "unknown", author, backend, model, effort, write, dispatch, independence, invoke, why,
prefer_external_when: dispatch === "native" ? [
"you need the strict JSON result contract, a ledger entry or a resumable run id",
"you need a hard timeout, a detached parallel writer, or automatic cross-vendor quota fallback",
@@ -583,10 +602,11 @@ function cmdRoute(argv) {
const suggested = stats ? suggestDepth(stats, kind) : null;
let depth = argv.depth && argv.depth !== true ? String(argv.depth) : (suggested ? suggested.depth : "single");
if (!DEPTHS.includes(depth)) die(`--depth must be ${DEPTHS.join("|")} (got ${depth})`);
- const author = base.backend === "claude" ? "codex" : "claude"; // base already placed the reviewer opposite the author
+ const author = base.author;
const reviewers = reviewComposition(depth, author, kind).map((r) => {
- const rr = resolveRoute({ ...argv, role: "reviewer", backend: r.backend, depth: undefined, diff: undefined });
- return { ...r, model: rr.model, effort: rr.effort, dispatch: rr.dispatch, invoke: rr.invoke };
+ const rr = resolveRoute({ ...argv, role: "reviewer", backend: r.backend, _composed: true, depth: undefined, diff: undefined });
+ const note = rr.why.find((w) => w.includes("falls back"));
+ return { ...r, backend: rr.backend, independent: rr.backend !== author, model: rr.model, effort: rr.effort, dispatch: rr.dispatch, invoke: rr.invoke, ...(note ? { note } : {}) };
});
const lead = depth === "led" ? (() => { const l = resolveRoute({ ...argv, role: "review-lead", backend: undefined, depth: undefined, diff: undefined }); return { backend: l.backend, model: l.model, effort: l.effort, dispatch: l.dispatch, invoke: l.invoke }; })() : null;
const sessions = reviewers.length + (lead ? 2 : 0);
@@ -899,7 +919,8 @@ lifecycle is orthogonal to status: running, or parked when the session can still
\`resume\`, or done when it cannot. status says how it ended, lifecycle whether you can go back to it.
Presets move the token-heavy roles (planner, implementer, researcher) to one family; reviewer and
-verifier always stay on the other family than the author. Precedence: --preset > DELEGATE_KIT_PRESET
+verifier follow the author: the other family when its CLI is installed, otherwise a fresh native
+worker of the parent's family (route marks it independent:false). Precedence: --preset > DELEGATE_KIT_PRESET
> ${CONFIG_FILE} > auto. Per-role model/effort defaults can be overridden in the same file under
"roles": { "": { "claude": ["model","effort"], "codex": ["model","effort"] } }; \`agent-run preset\` shows the effective table.