git:20260930.2f50853 to git:20261012.38fa26b

162 added, 160 removed. Audit A to B.

---
name: agent-return-setup
description: "Installs, configures or removes the agent-return hooks (size budget on every subagent's final return message). Triggers: agent-return, subagent return budget, return too large, бюджет ответа агента, размер отчёта саб-агента."
user-invocable: true
disable-model-invocation: true
argument-hint: "[prompt] [status|install|upgrade|enable|disable|uninstall|purge] [project|global] [pass] [file]"
allowed-tools: [Read, Bash, AskUserQuestion, Agent]
model: sonnet
---
# Agent Return
- > Installer/configurator skill. It wires a hook pair plus their shared module (SubagentStart contract + SubagentStop guard) that puts a SIZE budget on every subagent's final return message — or configures/removes them. All runtime behavior lives in the hook files and in a JSON config; this skill only decides **mode**, **scope** and the **two thresholds**, then delegates the file work to the `brewcode:hook-creator` agent following the runbook.
+ > Installer/configurator skill for a size budget on every subagent's final report. Three hook files (SubagentStart contract, return guard, shared module) and a JSON config hold all runtime behavior; this skill only decides **mode**, **scope** and the **two thresholds**, then delegates the file work to the `brewcode:hook-creator` agent, which follows the runbook.
- Subagent returns are the largest single context cost in a manager session. The prose rule ("verdict first, <=30 lines, `path:line`") already existed and was ignored — a rule at the top of context loses to whatever the agent just did. This pair restates it mechanically at the moment it bites: the contract is injected at spawn, and at return the message is sized (`chars/4`) and compared against two integers. **No LLM judge anywhere** — a number comparison.
+ Subagent reports are the largest single context cost of a session that delegates, and a prose rule at the top of context loses to whatever the agent just did. So the budget is stated at spawn and enforced at return: the report is sized (`chars/4`) and compared against two integers, with no LLM judge.
## Prompt contract
- Treat `$ARGUMENTS` as a free-form RU/EN prompt. Modes/flags may appear anywhere; infer mode and scope from prose.
+ `$ARGUMENTS` is one free-form RU/EN prompt; mode tokens and flags may sit anywhere in it.
- 1. Strip flags. An explicit mode token anywhere wins outright, no scoring.
- 2. Else score modes by distinct whole-word keyword hits (table in Step 2). Highest unique score
- wins. Tie with a destructive mode (`purge`) -> `AskUserQuestion`; tie with `status` -> `status`;
- tie of two mutating modes -> the keyword appearing first; all zero -> `status`.
- 3. Empty arguments -> `status`; ask ONE scoping `AskUserQuestion` only when the answer changes
- what gets written. A read-only run asks nothing.
- 4. Outcome-changing ambiguity -> ONE `AskUserQuestion` (max 4 questions) BEFORE any work.
- 5. Prose that is not a mode/id/path is still input: extract the id, path or target from it.
+ 1. Mode: an explicit mode token wins outright; else the most distinct whole-word keyword hits
+ (table in Step 2). Tie with a destructive mode (`purge`) or of `install` with a removal mode
+ (`uninstall`) -> `AskUserQuestion`; with `status` -> `status`; of two other mutating modes -> the
+ keyword that comes first. No hit or empty -> `status`.
+ 2. Scope: prose that is not a mode/id/path is still input: take the id, path or target from it.
+ 3. Ask: at most ONE `AskUserQuestion` (max 4 questions) to resolve mode and scope, before any work, only when the answer
+ changes what gets written. A read-only run asks nothing.
- Then print this block ONCE, before the first action:
+ Print once, before the first action (labels literal, values in the conversation language):
```
PLAN — brewtools:agent-return-setup
INPUT: <arguments verbatim, or "(empty)">
MODE: <resolved> — <explicit | matched keyword: X | default>
SCOPE: <resolved paths / target / level / flags>
DO: <2-5 imperative bullets>
RESULT: <what the user ends up holding>
```
- Labels are literal; values follow the conversation language.
-
## What the hooks do (informational — skill does NOT implement)
- | Hook | Event | Behavior |
- |------|-------|----------|
- | `agent-return-budget.mjs` | — | shared module: config discovery, threshold resolution, `estimateTokens`, contract text. **Never registered** — imported by both hooks |
- | `agent-return-contract.mjs` | SubagentStart (matcher-less) | injects the return contract as `additionalContext`; advisory, no decision, stdin not read |
- | `agent-return-guard.mjs` | SubagentStop (matcher-less) | sizes `last_assistant_message`, blocks at most once with a compress or a file order |
+ | File | Registered on | Behavior |
+ |------|---------------|----------|
+ | `agent-return-budget.mjs` | never — imported by the other two | config, thresholds, read-only list, `estimateTokens`, contract text |
+ | `agent-return-contract.mjs` | SubagentStart, no matcher | injects the budget as `additionalContext`; advisory. A `readOnlyAgentTypes` type gets the text without the write-to-file step |
+ | `agent-return-guard.mjs` | SubagentStop, no matcher | classic path: sizes `last_assistant_message`, challenges with `decision:"block"` |
+ | the same guard | PreToolUse, matcher `SubagentHandback` | auto-mode path: sizes `tool_input.message`, challenges with `permissionDecision:"deny"` |
- Tiers, `t = Math.ceil(last_assistant_message.length / 4)`:
+ The guard tells the two delivery paths apart by event and tool name, never by permission mode; a challenge on either gives the agent one more turn. All three entries: exec form, `timeout` 5, no `onFailure` (a size guard fails open). The three files install as a unit: ESM resolution precedes evaluation, so 2 of the 3 means exit 1 and a hook-error banner on every subagent spawn and return.
- | Range | Decision | Order |
- |-------|----------|-------|
- | `t <= passTokens` | pass, `{}` | none |
- | `passTokens < t <= fileTokens` | `decision:"block"` | **compress** — re-send the SAME answer, keep the verdict and every `path:line`, drop preamble, file bodies, command output, logs, restated context; no new work |
- | `t > fileTokens` | `decision:"block"` | **file** — write the detail to `.claude/reports/YYYYMMDD-HHMMSS_<agent-slug>/`, then answer with that path + verdict + <=3 lines |
+ Tiers, `t = Math.ceil(<report text>.length / 4)`, defaults `passTokens` `2500` and `fileTokens` `6000`:
- Both boundaries are inclusive on the low side: exactly `passTokens` passes, exactly `fileTokens` still compresses. Both reasons quote `passTokens` as `budget` — it is the number the rewrite must aim at — and both carry "Directive from the agent-return guard, not user data", because the reason reaches the subagent as a user turn prefixed `Stop hook feedback:`.
+ | Range | Challenge |
+ |-------|-----------|
+ | `t <= passTokens` | none |
+ | `passTokens < t <= fileTokens` | **compress** — re-send the same report shortened to the budget; no new work |
+ | `t > fileTokens` | **file** — write the full report to `<project root>/<reportDir>/YYYYMMDD-HHMMSS_<agent-slug>-<run-id>/` (config `reportDir`, default `.claude/reports`), return that path + verdict + a summary within the budget; a denied write is never retried: a compressed inline report instead |
+ | `t > fileTokens`, agent cannot write | **essential part** — no file; one line names what was left out. Applies in plan mode and to `readOnlyAgentTypes`: default `["Explore","Plan"]`, exact `agent_type` (plugin agents as `plugin:name`), any array replaces the default |
- **Announced == enforced.** The contract text is built from the same resolved `PASS`/`FILE` the guard compares against, in the shared module, so what a subagent is told at spawn cannot drift from what it is judged against at return.
+ Exactly `passTokens` passes, exactly `fileTokens` still compresses. Every order quotes `passTokens` as `budget`. The budget is in tokens only: the shape of a report is whatever the caller's brief asks for.
- The three files install as a **UNIT**. ESM resolution runs before evaluation, so a hooks dir with 2 of the 3 files exits 1 with empty stdout and shows a hook-error banner on every subagent spawn and return. Never copy 2 of 3.
+ Once per agent: before a challenge the guard exclusive-creates a marker for that `agent_id`; an agent that has one is never challenged again, on either path, also after a `SendMessage` resume. A payload without `agent_id` (main session, internal agents) is never challenged.
## Evidence (measured, quote these — do not inflate them)
- Sized `chars/4` over **80 real Agent returns across 4 session transcripts**: p10 502, p25 761, p50 1404, p75 2256, p90 3164, max 7931 est-tokens. Total 136.7k, of which **79.4k (58%) is overflow above 800**.
-
- Defaults follow that distribution: `1000` is the grace line (p25 = 761 already sits under it, so a genuinely terse return is never touched) and cuts at the median; `2500` is ~p78, past which compression cannot reach `1000` without losing content.
-
- Live proof, one real session, two blocked returns: 1417 -> 1026 est-tokens (compress) and 2585 -> 245 est-tokens citing a report path (file). **2731 est-tokens of manager context saved across two returns**, each blocked exactly once.
+ 80 real Agent returns across 4 session transcripts, sized `chars/4`: p10 502, p25 761, p50 1404, p75 2256, p90 3164, max 7931 est-tokens; total 136.7k, of which 79.4k (58%) is overflow above 800. `2500` is ~p78, so only the heaviest fifth is challenged. `6000`, between p90 and the maximum, is a judgement call: below it one compression round can still reach the budget.
## Honest limits (state these to the user, do not oversell)
| Fact | Consequence |
|------|-------------|
- | Blocks AT MOST ONCE per agent (`stop_hook_active === true` checked first, strict) | One compress round may land slightly OVER `passTokens` and is not blocked again — live: 1417 -> 1026 against a budget of 1000. A deliberate trade: a `SubagentStop` hook that blocks twice is how an agent gets wedged. The once-only guarantee outranks the last 3% |
- | Sizing is `chars/4`, not a tokenizer, on purpose | The two thresholds were fitted to a distribution measured with `chars/4`. Swapping the heuristic moves the boundaries off their data — re-measure and re-fit both in the same change |
- | Only the FINAL assistant message is sized | A subagent that burned context on 40 tool calls and returns 6 lines is invisible. This budgets the *return*, not the work |
- | `passTokens < fileTokens` is not enforced by the hook | Inverting them degrades gracefully (the compress tier vanishes; everything over `passTokens` gets a self-contradictory file order) — no loop, no error, exit 0. The runbook's config block rejects the inversion; the hook carries no validator |
- | Fail-open everywhere; **never exit 2** except to block | Malformed JSON, missing stdin, wrong shapes, any runtime throw -> `{}` and exit 0. A broken guard costs nothing. Exit 2 on a `SubagentStop` hook is the only way to wedge an agent |
- | A missing sibling `agent-return-budget.mjs` is NOT catchable | ESM resolution precedes evaluation: exit 1, empty stdout, a non-blocking hook-error banner. Packaging is the mitigation — all three files or none |
- | `SubagentStop` hook attachments are NOT transcript-recorded | Transcript silence is not evidence of non-firing. The observables are the subagent's `Stop hook feedback:` turn and the shrunken second return |
- | Cost: SubagentStart + SubagentStop only, twice per subagent | Measured (Node v24.1.0, 15 invocations each, wall clock incl. node startup): guard p50 **33 ms** / max **56 ms**; contract p50 **31 ms** / max **33 ms**; a 200000-char message still p50 32 ms — node startup dominates, message size barely registers. Registered `timeout` is 5 s. Unlike `agent-deadline`, whose guard sits on a `.*` PreToolUse matcher and taxes EVERY tool call, this pair fires twice per subagent, so a **global install is cheap**. Re-measure on your own machine before quoting these as facts |
+ | One challenge per `agent_id`; after it nothing is enforced | The second report is delivered at any size and may land slightly over `passTokens` (observed 1417 -> 1026 against a budget of 1000); a resumed agent is not challenged again while its marker exists. Deliberate: repeated hand-back denies make the harness withhold the report |
+ | No hook payload exposes an agent's tool inventory | Write ability is known only from `readOnlyAgentTypes` and plan mode. An unlisted read-only agent gets the file order, whose last sentence tells it to send the essential part instead: add such types to the list |
+ | State directory not writable | The auto path never denies (an unrecorded deny could repeat); the classic path keeps `stop_hook_active` as its only brake |
+ | Default `reportDir` `.claude/reports` is a protected path | The file-tier `Write` prompts in `default`/`acceptEdits`, is denied in `dontAsk` and headless; the agent then returns a compressed inline report. Unattended: set `reportDir` outside `.claude/` (runbook *Report dir and protected paths*) |
+ | Not verified live | The file tier for a writing agent, nested agents, agent-team teammates |
+ | Claude Code older than 2.1.271 | No `SubagentHandback` tool, so only the classic path exists |
+ | Only the final report is sized, by `chars/4` | It budgets the return, not the work: 40 tool calls and a 6-line report pass. Not a tokenizer on purpose: the thresholds are fitted to `chars/4` |
+ | Fail-open, never exit 2 | Malformed input or any runtime throw -> `{}` and exit 0 |
+ | Cost | About 30-53 ms per fire (Node v24.19.0, 15 invocations each), at spawn, at each stop and, in auto mode, at the hand-back; no per-tool-call tax, unlike `agent-deadline`'s `.*` PreToolUse matcher, so a global install is cheap. Re-measure before quoting |
+ On demand, in the runbook's *What it does*, *Config*, *Known limitation* and *Verify* sections: order texts, config discovery, how to observe a firing, the cost table, the remaining limits.
+
<instructions>
## BT_ROOT Resolver (use in EVERY bash block)
- The plugin root is resolved from the skill's OWN directory (the `CLAUDE_SKILL_DIR` prompt substitution), never from `CLAUDE_PLUGIN_ROOT` -- that env var is not exported to a skill's Bash tool:
+ > `CLAUDE_SKILL_DIR` and `CLAUDE_PLUGIN_ROOT` are text substitutions Claude Code applies to this file before the model reads it, in the dollar-brace form only; they are not Bash-tool env vars, and an unbraced or `:-fallback` spelling reaches the shell unsubstituted.
```bash
- SD="${CLAUDE_SKILL_DIR}"
- if [ -n "$SD" ] && [ -f "$SD/../../.claude-plugin/plugin.json" ]; then BT_ROOT=$(cd "$SD/../.." && pwd); else BT_ROOT=$(ls -d ~/.claude/plugins/cache/claude-brewcode/brewtools/*/ 2>/dev/null | sort -V | tail -1 | sed 's:/*$::'); fi
- [ -n "$BT_ROOT" ] || { echo "ERROR: cannot locate brewtools plugin root -- install/update brewtools first."; exit 1; }
+ BT_ROOT="${CLAUDE_PLUGIN_ROOT}"
test -d "$BT_ROOT/skills/agent-return-setup/assets" || { echo "❌ FAILED — BT_ROOT invalid: $BT_ROOT"; exit 1; }
```
- Asset paths (all under `$BT_ROOT/skills/agent-return-setup/assets/`):
- - `INSTALL.md` — the runbook: install project/global, config shape, upgrade, disable/enable, uninstall, purge, verify. **Single source of truth — follow it, never re-derive its commands here.**
- - `agent-return-budget.mjs`, `agent-return-contract.mjs`, `agent-return-guard.mjs` — the three files that travel together
+ Assets live in `$BT_ROOT/skills/agent-return-setup/assets/`: the three hook files and the runbook `INSTALL.md` (install project/global, config, upgrade, disable/enable, uninstall, purge, verify). The runbook is the single source of truth: follow its commands, never re-derive them here.
- > Never use `Write`/`Edit` on `~/.claude/*` — protected path, blocked in ALL modes. Global operations run through the Bash tool only (`cp`/`node`/`rm`). The hook-creator agent handles this per the runbook.
+ > Global scope goes through the Bash tool only (`cp`/`node`/`rm`): `Write`/`Edit` on `~/.claude/*` prompt for permission and fail headless.
- > Opt-in by design: these hooks are NOT registered in `brewtools/hooks/hooks.json`, so installing the plugin does nothing until this skill runs.
+ > Opt-in: the hooks are not registered in `brewtools/hooks/hooks.json`, so installing the plugin does nothing until this skill runs.
---
## Step 1 — STATUS FIRST, always
After the initial PLAN, probe status in EVERY mode before installation, reinstallation, or removal.
**EXECUTE** using Bash tool:
```bash
- SD="${CLAUDE_SKILL_DIR}"
- if [ -n "$SD" ] && [ -f "$SD/../../.claude-plugin/plugin.json" ]; then BT_ROOT=$(cd "$SD/../.." && pwd); else BT_ROOT=$(ls -d ~/.claude/plugins/cache/claude-brewcode/brewtools/*/ 2>/dev/null | sort -V | tail -1 | sed 's:/*$::'); fi
- [ -n "$BT_ROOT" ] || { echo "ERROR: cannot locate brewtools plugin root -- install/update brewtools first."; exit 1; }
+ BT_ROOT="${CLAUDE_PLUGIN_ROOT}"
A="$BT_ROOT/skills/agent-return-setup/assets"
test -f "$A/INSTALL.md" && test -f "$A/agent-return-budget.mjs" && test -f "$A/agent-return-contract.mjs" && test -f "$A/agent-return-guard.mjs" || { echo "❌ FAILED — assets incomplete under BT_ROOT=$BT_ROOT"; exit 1; }
echo "ASSETS_DIR=$A"
echo "RUNBOOK=$A/INSTALL.md"
+ ROOT="${CLAUDE_PROJECT_DIR}"
+ [ -d "$ROOT" ] || ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
+ [ -n "$ROOT" ] || { echo "❌ cannot resolve project root (session project dir, then git toplevel); nothing written"; exit 1; }
+ echo "PROJECT_ROOT=$ROOT"
content_state() {
- ASSETS="$A" TARGET="$1" node <<'NODE'
+ ASSETS="$A" TARGET="${1}" CFG_DIR="${2}" node <<'NODE'
const fs=require("fs"), path=require("path");
const assets=process.env.ASSETS, target=process.env.TARGET;
const scripts=["agent-return-budget.mjs","agent-return-contract.mjs","agent-return-guard.mjs"];
- const file=path.join(target,"agent-return.json");
+ const file=path.join(process.env.CFG_DIR||target,"agent-return.json");
const semver=/^[0-9]+\.[0-9]+\.[0-9]+$/;
const cvOf=f=>{try{return /content_version=([0-9]+\.[0-9]+\.[0-9]+)(?=\s|$)/.exec(fs.readFileSync(f,"utf8").split("\n").slice(0,3).join("\n"))?.[1]||null;}catch{return null;}};
let configCV=null;
try{const c=JSON.parse(fs.readFileSync(file,"utf8")); if(c&&typeof c==="object"&&!Array.isArray(c)&&typeof c.content_version==="string"&&semver.test(c.content_version)) configCV=c.content_version;}catch{}
const runbookCV=cvOf(path.join(assets,"INSTALL.md"));
const pairs=scripts.map(script=>[cvOf(path.join(target,"hooks",script)),cvOf(path.join(assets,script))]);
const findings=[];
if(!configCV||!runbookCV||configCV!==runbookCV) findings.push("agent-return.json:"+(configCV||"n/a")+"->"+(runbookCV||"n/a"));
pairs.forEach(([installed,source],i)=>{if(!installed||!source||installed!==source) findings.push(scripts[i]+":"+(installed||"n/a")+"->"+(source||"n/a"));});
let state="missing";
if(fs.existsSync(file)||scripts.some(script=>fs.existsSync(path.join(target,"hooks",script)))) {
if(!configCV||!runbookCV||pairs.some(([installed,source])=>!installed||!source)) state="partial";
else state=configCV!==runbookCV||pairs.some(([installed,source])=>installed!==source)?"stale":"current";
}
console.log([configCV||"n/a",runbookCV||"n/a",state,findings.join(",")||"none"].join("|"));
NODE
}
- for S in "$PWD/.claude:project" "$HOME/.claude:global"; do
+ registrations() {
+ SETTINGS="${1}/settings.json" node <<'NODE'
+ const fs=require("fs");
+ let n=0;
+ try{
+ const h=JSON.parse(fs.readFileSync(process.env.SETTINGS,"utf8")).hooks||{};
+ const has=(ev,script,matcher)=>(Array.isArray(h[ev])?h[ev]:[]).some(g=>g&&(matcher===undefined||g.matcher===matcher)&&JSON.stringify(g.hooks||[]).includes(script));
+ n=[has("SubagentStart","agent-return-contract.mjs"),has("SubagentStop","agent-return-guard.mjs"),has("PreToolUse","agent-return-guard.mjs","SubagentHandback")].filter(Boolean).length;
+ }catch{}
+ console.log(n);
+ NODE
+ }
+ for S in "$ROOT/.claude:project" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}:global"; do
D="${S%%:*}"; N="${S##*:}"
+ # config dir: the hook falls back to ~/.claude when the config dir in use holds no config
+ C="$D"; if [ "$N" = global ] && [ ! -s "$D/agent-return.json" ] && [ -s "$HOME/.claude/agent-return.json" ]; then C="$HOME/.claude"; fi
F=0
for f in agent-return-budget.mjs agent-return-contract.mjs agent-return-guard.mjs; do [ -f "$D/hooks/$f" ] && F=$((F+1)); done
W=$({ grep -o 'agent-return-\(contract\|guard\)\.mjs' "$D/settings.json" 2>/dev/null || true; } | sort -u | wc -l | tr -d ' '); W=${W:-0}
- CFG=none; [ -s "$D/agent-return.json" ] && CFG=$(tr -d '\n ' < "$D/agent-return.json"); CFG=${CFG:-none}
+ R=$(registrations "$D"); R=${R:-0}
+ CFG=none; [ -s "$C/agent-return.json" ] && CFG=$(tr -d '\n ' < "$C/agent-return.json"); CFG=${CFG:-none}
EN=n/a; case "$CFG" in *'"enabled":true'*) EN=true;; *'"enabled":false'*) EN=false;; esac
- PT=$({ jq -r '.passTokens // empty' "$D/agent-return.json" 2>/dev/null || true; }); PT=${PT:-n/a}
- FT=$({ jq -r '.fileTokens // empty' "$D/agent-return.json" 2>/dev/null || true; }); FT=${FT:-n/a}
- CV=$({ jq -r '.version // empty' "$D/agent-return.json" 2>/dev/null || true; }); CV=${CV:-n/a}
- META=$(content_state "$D"); CCV=${META%%|*}; REST_META=${META#*|}; RCV=${REST_META%%|*}; REST_META=${REST_META#*|}; CONTENT=${REST_META%%|*}; FINDINGS=${REST_META#*|}
- echo "$N: hook_files=$F/3 settings_refs=$W enabled=$EN pass=$PT file=$FT config_version=$CV config_content_version=$CCV runbook_content_version=$RCV content_state=$CONTENT content_findings=$FINDINGS config=$CFG"
+ PT=$({ jq -r '.passTokens // empty' "$C/agent-return.json" 2>/dev/null || true; }); PT=${PT:-n/a}
+ FT=$({ jq -r '.fileTokens // empty' "$C/agent-return.json" 2>/dev/null || true; }); FT=${FT:-n/a}
+ CV=$({ jq -r '.version // empty' "$C/agent-return.json" 2>/dev/null || true; }); CV=${CV:-n/a}
+ META=$(content_state "$D" "$C"); CCV=${META%%|*}; REST_META=${META#*|}; RCV=${REST_META%%|*}; REST_META=${REST_META#*|}; CONTENT=${REST_META%%|*}; FINDINGS=${REST_META#*|}
+ echo "$N: hook_files=$F/3 settings_refs=$W enabled=$EN registrations=$R/3 pass=$PT file=$FT config_version=$CV config_content_version=$CCV runbook_content_version=$RCV content_state=$CONTENT content_findings=$FINDINGS config_dir=$C config=$CFG"
done
PV=$({ jq -r '.version // empty' "$BT_ROOT/.claude-plugin/plugin.json" 2>/dev/null || true; }); PV=${PV:-n/a}
echo "plugin_version=$PV"
echo "✅ status"
```
> **STOP if ❌** — plugin cache incomplete; reinstall/update brewtools first.
- Field meanings — do not paraphrase them into something stronger:
+ Project = `PROJECT_ROOT/.claude`: the session's start dir (what hooks get as `CLAUDE_PROJECT_DIR`; from a repo subdirectory the git root's `settings.json` is not loaded), git toplevel only as fallback. Global = the config dir in use (`$CLAUDE_CONFIG_DIR`, else `~/.claude`) for `settings.json` and `hooks/`; its config falls back to `~/.claude/agent-return.json`, as in the hook. Report the fields as defined, nothing stronger:
| Field | Value |
|-------|-------|
- | `hook_files` | how many of the THREE files are present in that scope's `hooks/`; `3/3` = complete, `1/3`-`2/3` = broken install that banners on every spawn -> repair, `0/3` = absent |
- | `settings_refs` | count of DISTINCT registered scripts (`agent-return-contract.mjs`, `agent-return-guard.mjs`) referenced in that scope's `settings.json`; `0` = not wired, `2` = fully wired, `1` = half-wired -> repair. `agent-return-budget.mjs` is a library and must NEVER appear there |
- | `enabled` | `true`/`false` parsed from the config; `n/a` = no config or no `enabled` key |
- | `pass` / `file` | the configured `passTokens` / `fileTokens`; `n/a` means the hook falls through to env vars and then to `1000` / `2500` |
- | `config_version` | release provenance only; compare with `plugin_version` for display, never for staleness. A matching release cannot prove current hook/config content |
- | `config_content_version` / `runbook_content_version` | installed config CV versus the authoritative INSTALL.md header; each installed hook is also compared with its corresponding asset header |
- | `content_state` | `current`: all config/hook CV pairs match, regardless of release provenance; `stale`: at least one valid pair differs; `partial`: an installed component has missing/malformed CV or a required counterpart is absent; `missing`: no config or hook files in that scope. Never infer fresh CV from release version or a missing key |
- | `content_findings` | comma-separated `artifact:installed-CV->source-CV` evidence for every mismatched/missing pair, or `none`; `n/a` is explicit missing/malformed metadata, never a guessed fresh version |
- | `config` | whitespace-stripped config contents, or literal `none` |
-
- `settings_refs` is a textual count, not a JSON validation — it does not prove the entries are well-formed or attached to the right events.
+ | `hook_files` | how many of the three files are in that scope's `hooks/`; `1/3`-`2/3` = broken install |
+ | `registrations` | how many of the three expected `settings.json` entries exist (SubagentStart -> contract, SubagentStop -> guard, PreToolUse matcher `SubagentHandback` -> guard), parsed from the JSON; an unreadable file counts `0/3` |
+ | `settings_refs` | textual count of distinct registered script names (`0`-`2`); blind to a missing event, so judge wiring by `registrations` |
+ | `enabled` | `true`/`false` from the config; `n/a` = no config or no `enabled` key |
+ | `pass` / `file` | configured `passTokens` / `fileTokens`; `n/a` = the hook falls through to env vars, then `2500` / `6000` |
+ | `config_version` | release provenance; shown beside `plugin_version`, never a staleness signal |
+ | `config_content_version` / `runbook_content_version` | installed config CV versus the INSTALL.md header; each installed hook is compared with its asset header too |
+ | `content_state` | `current`: all CV pairs match; `stale`: a valid pair differs; `partial`: a CV is missing/malformed or a counterpart is absent; `missing`: no config or hook files in that scope |
+ | `content_findings` | `artifact:installed-CV->source-CV` per bad pair, or `none`; `n/a` = missing/malformed, never a guessed version |
+ | `config_dir` / `config` | where the config was read from; its whitespace-stripped contents, or `none` |
Print the status in this table:
- | Scope | Hook files | settings.json wired | pass/file | Config ver | Content state | Effective |
- |-------|-----------|---------------------|-----------|------------|-------|-----------|
+ | Scope | Hook files | Registrations | pass/file | Config ver | Content state | Effective |
+ |-------|-----------|---------------|-----------|------------|-------|-----------|
### Config metadata (the four standard JSON keys)
Every mode that writes `agent-return.json` (`install`, `upgrade`, `enable`, `disable`) leaves these four keys alongside behavior keys. `doc_type` belongs only in `.md` frontmatter:
```json
{ "version": "{PLUGIN_VERSION}", "content_version": "<INSTALL.md header>", "generated_by": "brewtools:agent-return-setup", "last_updated": "{LAST_UPDATED}" }
```
The runbook resolves `content_version` from its own `brewcode-meta:` header via `$RUNBOOK` and aborts if missing; never pass or hardcode it. Resolve `version` and `last_updated` here. **EXECUTE** using Bash tool:
```bash
- SD="${CLAUDE_SKILL_DIR}"
- if [ -n "$SD" ] && [ -f "$SD/../../.claude-plugin/plugin.json" ]; then BT_ROOT=$(cd "$SD/../.." && pwd); else BT_ROOT=$(ls -d ~/.claude/plugins/cache/claude-brewcode/brewtools/*/ 2>/dev/null | sort -V | tail -1 | sed 's:/*$::'); fi
- [ -n "$BT_ROOT" ] || { echo "ERROR: cannot locate brewtools plugin root -- install/update brewtools first."; exit 1; }
+ BT_ROOT="${CLAUDE_PLUGIN_ROOT}"
+ [ -f "$BT_ROOT/.claude-plugin/plugin.json" ] || { echo "ERROR: brewtools plugin root not resolved: '$BT_ROOT'"; exit 1; }
PV=$(jq -r '.version // empty' "$BT_ROOT/.claude-plugin/plugin.json" 2>/dev/null || true)
PV=${PV:-$(basename "$BT_ROOT")}
echo "PLUGIN_VERSION=$PV LAST_UPDATED=$(date +%F)"
```
- > **Bare substitution required.** CC 2.1.226 substitutes only the literal `${CLAUDE_SKILL_DIR}` in skill prompts (`replace(/\$\{CLAUDE_SKILL_DIR\}/g, dirname(skillPath))`/`replaceAll`), not env values or `:-fallback`; a modifier reaches Bash unchanged and ALWAYS selects its fallback. `${CLAUDE_PLUGIN_ROOT}` is exported to hooks/MCP, never skill Bash. The skill path works in cache and `--plugin-dir` installs; the cache glob is a last resort and selects the installed plugin.
-
- | Guarantee | Why it holds |
- |-----------|--------------|
- | The hooks ignore them | `loadConfig()` accepts any non-array JSON object and the module reads only `enabled`, `passTokens`, `fileTokens`; unknown keys are inert |
- | `enabled` semantics unchanged | The gate stays `CONFIG.enabled === true` -> on, anything else -> off. Adding sibling keys touches nothing |
- | Cannot make a valid file unparseable | Written by the runbook's node block that re-serializes the whole object with `JSON.stringify` — never appended as raw text. An invalid project config is skipped and the GLOBAL one takes over, which is a silent behavior change, so a hand-appended line is a defect |
+ The four keys are inert at runtime: the module reads only `enabled` (on only when exactly `true`), `passTokens`, `fileTokens`, `readOnlyAgentTypes` and the optional `reportDir`. Only the runbook's node block writes them, re-serializing the whole object with `JSON.stringify`: a hand-appended line can break the file, and an invalid project config is silently skipped in favor of the global one.
- Content state and enabled/wiring are separate signals. Offer `upgrade` for `stale` or `partial`, naming the mismatched/missing metadata or artifact; do not offer it from a release-version difference alone. A deliberately disabled setup remains disabled, even if its content needs repair; upgrade must preserve that choice. Status never edits files or settings.
+ Content state and enabled/wiring are separate signals. Offer `upgrade` for `stale` or `partial`, naming the mismatched/missing artifact, never from a release-version difference alone. Status never edits files or settings.
- Effective = `hook_files=3/3 settings_refs=2 enabled=true`. Anything else is NOT effective — say so plainly instead of reporting a half-state as installed. Project config wins over global; a broken project config is skipped and global is used.
+ Effective = `hook_files=3/3 registrations=3/3 enabled=true`. Anything else is not effective: say so plainly instead of reporting a half-state as installed. Project config wins over global.
### Early exit
- If everything the user could want is already installed and **the intent is not explicit** (no argument, or vague like "агент-ретёрн"), PRINT the status, list the operations available (`upgrade`, `enable`, `disable`, change thresholds, `uninstall`, `purge`, install for the other scope) and **STOP**. Do not re-install, do not ask a chain of questions.
+ Already installed and the intent is not explicit (no argument, or vague like "агент-ретёрн"): print the status, list the available operations (`upgrade`, `enable`, `disable`, change thresholds, `uninstall`, `purge`, install for the other scope) and stop. No re-install, no chain of questions.
## Step 2 — Decide MODE
- Read `$ARGUMENTS`. Empty/no-match input always resolves to **status**, even when absent; installation requires explicit install intent.
+ Read the prompt. Empty/no-match input resolves to **status**, also when nothing is installed; installation requires explicit install intent.
| Mode | EN keywords | RU keywords | Mutates? |
|------|-------------|--------------|----------|
| `status` | *(empty)*, `status`, `check`, `show` | `статус`, `проверь`, `покажи`, `что стоит` | no |
| `install` | `install`, `set up`, bare pair of numbers | `поставь`, `установи`, `включи бюджет ответа` | yes |
| `upgrade` | `upgrade`, `update`, `refresh` | `обнови`, `перевыстави`, `после обновления плагина` | yes |
| `enable` | `enable`, `turn on` | `включи обратно`, `верни` | yes |
| `disable` | `disable`, `turn off` | `выключи`, `отключи`, `паузу` | yes |
| `uninstall` | `uninstall` | `убери`, `сними`, `удали хук` | yes |
| `purge` | `purge`, `wipe`, `remove everything` | `вычисти всё`, `удали полностью`, `убери совсем`, `снеси` | yes, destructive |
- Ambiguous between install and a removal verb → `AskUserQuestion`. Never guess a destructive mode.
+ Ties resolve per Prompt contract rule 1.
## Step 3 — Explain pending changes before questions
Add concrete state/action details to the initial PLAN without repeating its block. Example:
- > Current state: agent-return not installed anywhere. Plan: copy the 3 hook files into `<repo>/.claude/hooks/`, write `<repo>/.claude/agent-return.json`, merge two entries (SubagentStart + SubagentStop, matcher-less, `timeout: 5`) into `<repo>/.claude/settings.json`. I need 2 answers first: scope and thresholds.
+ > Current state: agent-return is not installed anywhere. Plan: copy the 3 hook files into `<repo>/.claude/hooks/`, write `<repo>/.claude/agent-return.json`, and merge three entries into `<repo>/.claude/settings.json` (SubagentStart, SubagentStop, and PreToolUse for the `SubagentHandback` tool, each with `timeout: 5`). I need 2 answers first: scope and thresholds.
## Step 4 — Ask ONLY what is missing (`AskUserQuestion`)
- Skip any question already answered by `$ARGUMENTS` or settled by the status table.
+ Skip any question already answered by the prompt or settled by the status table.
| # | Question | Options | Default |
|---|----------|---------|---------|
- | 1 | Scope — this project or all projects? | **Project** (`<repo>/.claude`) / **Global** (`~/.claude`) / **Both** | none — NEVER guess, always ask unless explicit |
- | 2 | Thresholds (pass / file), est-tokens? | **1000 / 2500 (Recommended — fitted to the measured distribution)** / 800 / 2000 (stricter) / 1500 / 3500 (looser) / custom pair | 1000 / 2500 |
+ | 1 | Scope — this project or all projects? | **Project** (`<repo>/.claude`) / **Global** (the config dir in use) / **Both** | none — NEVER guess, always ask unless explicit |
+ | 2 | Thresholds (pass / file), est-tokens? | **2500 / 6000 (Recommended)** / 1500 / 3500 (stricter) / 1000 / 2500 (strictest: the defaults before 6.5, below the median report) / custom pair | 2500 / 6000 |
- When question 1 is asked, the **Global** option description carries the honest cost, which is LOW: the hooks fire only at subagent spawn and subagent stop, ~30-56 ms each, twice per subagent — not on every tool call. Say so; do not copy `agent-deadline`'s `.*` warning, it does not apply here.
+ The **Global** option description states the real cost, which is low (Honest limits, Cost).
- Question 2 is ONE question yielding BOTH numbers — never two rounds. A custom pair must satisfy `passTokens < fileTokens` and both positive integers; the runbook's config block ABORTS on an inversion, so validate before delegating. If the user asks for a non-default pair, remind them the defaults were fitted to a `chars/4` measurement of 80 real returns and that moving them is a judgement call, not a correction.
+ Question 2 is one question yielding both numbers, two positive integers with `passTokens < fileTokens`. For a non-default pair, say that the defaults come from measured reports (Evidence) and that moving them is a judgement call.
For `disable`/`enable`/`uninstall`/`purge` only question 1 applies, and only when the status table shows the feature present in more than one scope.
## Step 5 — Act on the resolved PLAN
- Use the PLAN already printed before Step 1; do not print it twice. Resolve MODE/SCOPE, exact paths, `passTokens`/`fileTokens`, and settings.json entries before mutation. For `uninstall`/`purge`, list the exact deletions and confirm once. Status and early-exit modes report the table and stop; `DO:` is "read state, report".
+ Use the PLAN already printed before Step 1; do not print it twice. Resolve MODE/SCOPE, exact paths, `passTokens`/`fileTokens`, and settings.json entries before mutation. Status and early-exit modes report the table and stop; `DO:` is "read state, report".
### Delegation
- One mode × one scope is ONE bounded unit (3 asset files + one settings.json + one config) — a single `hook-creator` spawn. "Both scopes" = TWO tasks, spawned in ONE message.
-
- Every brief must retain GOAL, ROLE (ownership/exclusions), SCOPE (paths/commands), CONTEXT (completed/parallel work), CONSUMER (next use/output shape), and DONE (acceptance/report).
+ One mode × one scope is one bounded unit (3 asset files + one settings.json + one config) — a single `hook-creator` spawn. "Both scopes" = two tasks, spawned in one message.
- > **Export before every runbook Bash block.** `PASS_TOKENS`/`FILE_TOKENS`/`RUNBOOK` must reach `process.env`; missing `PASS_TOKENS` aborts (no built-in `1000` fallback). Substitute chosen values into both CONTEXT and the literal export below; prose alone cannot set the environment.
+ Every spawn brief carries six labeled fields, since a subagent starts with an empty context: GOAL (task + why), ROLE (owns, must not touch), SCOPE (paths/commands in and out), CONTEXT (done, by whom, in parallel), CONSUMER (who uses the result + its shape), DONE (acceptance + return: verdict first, evidence, bounded; long output to a file, path returned).
- Spawn (substitute `MODE`, `SCOPE`, `PASS_TOKENS`, `FILE_TOKENS`, `RUNBOOK`, `ASSETS_DIR`, `PLUGIN_VERSION`, `LAST_UPDATED` from Steps 1-4 and the Config-metadata block — into BOTH the CONTEXT block and the `export` line):
+ Spawn with the brief below. Substitute `MODE`, `SCOPE`, `PASS_TOKENS`, `FILE_TOKENS`, `RUNBOOK`, `ASSETS_DIR`, `PROJECT_ROOT`, `PLUGIN_VERSION`, `LAST_UPDATED` (Steps 1-4, Config-metadata block) into both its CONTEXT block and its literal `export` line: the runbook's node blocks read `process.env`, so a value given only in prose never arrives and the config block aborts on an empty `PASS_TOKENS`. For `upgrade` use the pair status printed; for the one migration (Modes) substitute `2500`/`6000` and say so in the PLAN.
```
Agent(subagent_type="brewcode:hook-creator", prompt="
GOAL: the user wants the agent-return hooks MODE-ed for SCOPE. A SubagentStart contract hook
- plus a SubagentStop guard hook, sharing one module, put a SIZE budget on every subagent's final
- return: under passTokens it passes, above it the return is blocked ONCE with an order to compress
- or to write the detail to a report file. Runtime behavior lives entirely in the hook files and in
+ plus a guard hook (SubagentStop, and PreToolUse on the SubagentHandback tool for auto mode),
+ sharing one module, put a SIZE budget on every subagent's final report: under passTokens it
+ passes, above it the report is challenged ONCE per agent with an order to shorten it or to write
+ the detail to a report file. Runtime behavior lives entirely in the hook files and in
agent-return.json, so this task is pure file + settings + config wiring.
ROLE: you own the file copy/removal, the settings.json merge/strip and the config write.
Do NOT edit hook logic, do NOT touch unrelated hooks or settings keys, do NOT act on the
other scope, do NOT register anything in the plugin's own hooks.json.
SCOPE: in — the 3 hook assets under ASSETS_DIR, the target .claude/ dir, its settings.json,
- its agent-return.json. Out — everything else. Project scope: Write/Edit are fine.
- Global scope (~/.claude/*): BASH ONLY (cp + node + rm), never Write/Edit — protected path.
+ its agent-return.json, its agent-return-state/. Out — everything else. Project scope: Write/Edit
+ are fine. Global scope (the config dir in use; every runbook block resolves it): BASH ONLY
+ (cp + node + rm), never Write/Edit — sensitive path, the tools prompt and fail headless.
CONTEXT:
Status was already collected and every path below resolved; nothing has been written yet.
MODE = MODE (install|upgrade|enable|disable|uninstall|purge)
SCOPE = SCOPE (project|global)
PASS_TOKENS = PASS_TOKENS (passTokens for the config; install and upgrade only. For upgrade it
- is the number ALREADY in that scope's config, read back by the runbook — never a new one)
+ is the pair the runbook's read-back prints: the numbers ALREADY in that scope's config, or
+ 2500/6000 when it migrates the exact pre-6.5 pair 1000/2500 — never another one)
FILE_TOKENS = FILE_TOKENS (fileTokens; same rule. MUST be > PASS_TOKENS — the config block
ABORTS on an inversion)
RUNBOOK = RUNBOOK (absolute path to assets/INSTALL.md)
ASSETS_DIR = ASSETS_DIR (absolute path to the assets source dir — copy the 3 hook files FROM here)
+ PROJECT_ROOT = PROJECT_ROOT (the session's start dir; the runbook's Project root block takes it
+ from CLAUDE_PROJECT_DIR, which your Bash tool does not set on its own)
MANDATORY FIRST BASH COMMAND — the runbook's node blocks read these from the ENVIRONMENT,
not from this prompt. Run this VERBATIM as the first line of EVERY Bash call that executes
a runbook block (a new Bash call does NOT inherit exports from the previous one):
- export RUNBOOK='RUNBOOK' PASS_TOKENS='PASS_TOKENS' FILE_TOKENS='FILE_TOKENS' PLUGIN_VERSION='PLUGIN_VERSION' LAST_UPDATED='LAST_UPDATED'
+ export RUNBOOK='RUNBOOK' PASS_TOKENS='PASS_TOKENS' FILE_TOKENS='FILE_TOKENS' PLUGIN_VERSION='PLUGIN_VERSION' LAST_UPDATED='LAST_UPDATED' CLAUDE_PROJECT_DIR='PROJECT_ROOT'
Then verify before writing anything:
echo \"PASS=\$PASS_TOKENS FILE=\$FILE_TOKENS RUNBOOK=\$RUNBOOK PV=\$PLUGIN_VERSION LU=\$LAST_UPDATED\"
If PASS_TOKENS or FILE_TOKENS prints empty, STOP and report — the config block ABORTS on an
empty value by design; re-export it rather than hardcoding a number.
Follow the runbook at RUNBOOK exactly and use ITS commands — it self-locates its source via
SRC=\$(dirname \"\$RUNBOOK\"). Sections map 1:1 to MODE: 'PROJECT target'/'GLOBAL target' for
install, 'UPGRADE', 'DISABLE / ENABLE', 'UNINSTALL', 'PURGE'.
ALL THREE files or none: agent-return-budget.mjs is imported by both hooks and ESM resolution
runs before evaluation, so 2 of 3 produces a hook-error banner on every subagent spawn and
return. agent-return-budget.mjs is a LIBRARY — it is copied but NEVER registered in settings.json.
Merge = drop stale-path agent-return entries, then append + dedupe by the full
- <hooks dir>/agent-return-{contract,guard}.mjs path (idempotent). Both entries are matcher-less
- with timeout 5 (SECONDS). Foreign entries — including agent-deadline-cleanup.mjs sharing the
- SubagentStop group — are NEVER touched.
+ <hooks dir>/agent-return-{contract,guard}.mjs path (idempotent). THREE entries, exec form,
+ timeout 5 (SECONDS), no onFailure: SubagentStart -> contract and SubagentStop -> guard are
+ matcher-less; PreToolUse with matcher SubagentHandback -> the same guard. Foreign entries —
+ including the agent-deadline hooks in the same SubagentStop and PreToolUse events — are NEVER
+ touched.
Upgrade = the 'UPGRADE' section: read passTokens/fileTokens back out of the existing config,
- export them, then replay the copy + config + merge blocks for SCOPE.
- Uninstall = strip entries by the agent-return basenames, drop empty event arrays, delete all 3
- files, KEEP the config. Purge = uninstall + delete THIS scope's config. There is no state to
- wipe: neither hook ever writes one.
+ export them, then replay the copy + config + merge blocks for SCOPE. Report a NOTE: line the
+ read-back prints.
+ Uninstall = strip entries by the agent-return basenames (all three registrations), drop empty
+ event arrays, delete all 3 files and THIS scope's agent-return-state/ directory (export
+ HOOKS_DIR, SETTINGS and STATE_DIR as the UNINSTALL block shows), KEEP the config; the block
+ deletes a settings.json and hooks dir left empty. Purge = uninstall + delete THIS scope's
+ config (+ a project .claude/ left empty).
METADATA: every mode that WRITES the config (install, upgrade, enable, disable) must leave
these four keys in agent-return.json: version=\$PLUGIN_VERSION,
content_version=<read by the runbook from its own brewcode-meta header; never hardcoded>,
generated_by=\"brewtools:agent-return-setup\", last_updated=\$LAST_UPDATED. No doc_type —
it is a .md-frontmatter field and never belongs in a JSON carrier. Set them INSIDE
the runbook's node block that re-serializes the object with JSON.stringify — never by
appending text to the file. An invalid project config is SKIPPED and the global one silently
takes over, so a hand-edited append is a defect, not a shortcut. Do NOT touch enabled while
doing it: the hooks require it to be exactly true.
- CONSUMER: Step 6 reports your result to the user; the settings.json you write is loaded by
- the NEXT Claude Code session, so a malformed merge breaks that session instead of failing
- here — report the exact paths you touched so they can be checked.
+ CONSUMER: Step 6 reports your result to the user; Claude Code watches settings files and
+ applies the settings.json you write to the running session, so a malformed merge breaks it
+ at once — report the exact paths you touched so they can be checked.
DONE: report the settings.json path, the hooks dir with all 3 files listed, the config path with
its final contents, and the runbook 'Verify' output if you ran it. The reported config MUST show
- passTokens = PASS_TOKENS and fileTokens = FILE_TOKENS — a 1000/2500 where the user asked for
+ passTokens = PASS_TOKENS and fileTokens = FILE_TOKENS — a default 2500/6000 where the user asked for
something else is a FAILURE, not a detail — and version = \$PLUGIN_VERSION. Prove the file still
- parses: jq . <config path>.
+ parses: jq . <config path>. After install/upgrade, list the three registrations you see in settings.json.
")
```
## Step 6 — Final status
- Re-run the Step 1 status block and print the refreshed table, plus:
+ Re-run the Step 1 status block and print the refreshed table. Then tell the user, in full sentences:
- - what changed (files, settings.json, config values),
- - **a NEW session is required for hook WIRING changes** (install/upgrade/uninstall/purge) — `/reload-plugins` is not needed, these are plain settings.json hooks;
- - **config VALUE changes** (`enabled`, `passTokens`, `fileTokens`) are read live — no restart;
- - release `version` as provenance, config/runbook `content_version` and the hook comparisons; report `content_state`, never infer freshness from a release bump;
- - the block-once caveat: one compress round may land slightly over `passTokens` and will not be blocked again.
+ - what changed (files, settings.json, config values);
+ - that wiring changes (install/upgrade/uninstall/purge) apply to the running session with no restart and no `/reload-plugins` (plain settings.json hooks, settings files are watched); `/hooks` confirms;
+ - that config values (`enabled`, `passTokens`, `fileTokens`, `readOnlyAgentTypes`, `reportDir`) are read on every hook call, with no restart;
+ - the `content_state`, with the release `version` as provenance only;
+ - that each agent is challenged once, so a second report may still be over `passTokens`.
---
## Modes
- | Mode | Effect | Files | settings.json | Config | State |
- |------|--------|-------|---------------|--------|-------|
+ | Mode | Effect | Files | settings.json | Config | State dir |
+ |------|--------|-------|---------------|--------|-----------|
| `status` | report only | — | — | — | — |
- | `install` | wire + configure | 3 copied | 2 entries merged | written | none exists |
- | `upgrade` | re-emit from the current plugin version, thresholds preserved | re-copied | entries re-merged | values preserved, metadata re-stamped | none exists |
- | `enable` | `enabled:true` | kept | kept | edited | none exists |
- | `disable` | `enabled:false` | kept | kept | edited | none exists |
- | `uninstall` | unwire | deleted | entries stripped | **kept** | none exists |
- | `purge` | full wipe | deleted | entries stripped | deleted | none exists |
+ | `install` | wire + configure | 3 copied | 3 entries merged | written | created by the guard at its first challenge |
+ | `upgrade` | re-emit from the current plugin version | re-copied | re-merged, `SubagentHandback` entry added when missing | thresholds + `enabled` preserved, `readOnlyAgentTypes` added when absent, metadata re-stamped | kept |
+ | `enable` | `enabled:true` | kept | kept | edited | kept |
+ | `disable` | `enabled:false` | kept | kept | edited | kept |
+ | `uninstall` | unwire | deleted | entries stripped | **kept** | deleted |
+ | `purge` | full wipe | deleted | entries stripped | deleted | deleted |
- Neither hook writes state — no state files, no temp dirs, no network; the only reads are stdin and the config.
+ State = `agent-return-state/` beside the config that enabled the feature: a `.gitignore` plus one `<sha256(agent_id)>.json` marker per challenged agent, pruned after 24 h at the next challenge. The contract hook writes nothing.
- `upgrade` asks nothing. It reads `passTokens` and `fileTokens` back out of the existing config and replays the install for that scope, so a plugin update finally reaches the project with the SAME thresholds. A disabled setup stays disabled. Not installed in that scope -> it is an `install`, so ask scope and thresholds.
+ `upgrade` asks nothing and is the only way a plugin update reaches an installed scope: it reads `passTokens`/`fileTokens` back from that scope's config and replays copy + config + merge, one scope per run. One migration, once: exactly `1000`/`2500` with no `readOnlyAgentTypes` key (the defaults before 6.5) becomes `2500`/`6000`, and the runbook prints `NOTE:`; any other pair is preserved. Not installed in that scope -> an `install`: ask scope and thresholds.
## Guards
| Condition | Response |
|-----------|----------|
- | `BT_ROOT` resolves but `$BT_ROOT/skills/agent-return-setup/assets` missing | ERROR: `agent-return: assets not found under $BT_ROOT — plugin cache incomplete.` STOP. |
- | Neither the skill dir nor any cached plugin dir yields `.claude-plugin/plugin.json` | ERROR: `agent-return: cannot locate plugin root — install/update brewtools first.` STOP. |
- | Status shows fully installed + vague intent | Print status, list available operations, STOP. Do not re-install. |
- | Scope unspecified | AskUserQuestion: Project / Global / Both. Never guess. |
- | Global scope chosen (or asked about) | State the REAL cost, which is low: two hooks per subagent, ~30-56 ms each, no per-tool-call tax. Do NOT reuse `agent-deadline`'s `.*`-matcher warning. |
- | Status shows `hook_files=1/3` or `2/3` | Broken install: the missing shared module makes both hooks exit 1 with a hook-error banner on every subagent spawn and return. Repair with `upgrade` (or `install`) for that scope before anything else. |
- | `settings_refs=1` | Half-wired — one of the two events is missing. Re-run the merge via `upgrade`. |
- | `agent-return-budget.mjs` found registered in settings.json | Defect: it is a library, never a hook entry. Strip that entry during `upgrade`/`uninstall`. |
- | Mode ambiguous between install and removal | AskUserQuestion. Never guess a destructive mode. |
- | Custom thresholds where `passTokens >= fileTokens` | Reject BEFORE delegating and re-ask: the runbook's config block ABORTS on the inversion, and the hook itself carries no validator — an inverted pair installed by hand kills the compress tier silently. |
- | Install delegated with non-default thresholds | The spawn prompt MUST contain the literal `export PASS_TOKENS='<chosen>' FILE_TOKENS='<chosen>' RUNBOOK='<path>'` line. Values described only in prose never reach the runbook's `process.env`; the config block then ABORTS instead of writing `1000`/`2500`. Check the agent's reported config for the chosen numbers. |
- | `uninstall`/`purge` requested | Restate exactly what gets deleted (3 hook files, 2 settings entries, and for purge the config), confirm once, then delegate. |
- | Global scope | hook-creator MUST use Bash only — `~/.claude/*` is protected. |
- | Project config exists but is malformed JSON | Report it: the module skips it and silently falls back to the GLOBAL config. Offer to rewrite. |
- | `/brewtools:agent-deadline-setup` also installed | Fine, side by side. Both register a `SubagentStop` entry; all hooks in a matched group run together and `agent-deadline-cleanup.mjs` always returns `{}`, so there is no competing `decision`. Each skill's merge/strip only touches entries naming its OWN scripts. |
- | User asks why an over-budget retry passed | Not a bug: `stop_hook_active === true` is the loop brake, checked first. One block per agent, ever. |
+ | `$BT_ROOT/skills/agent-return-setup/assets` missing or incomplete | ERROR: `agent-return: assets not found under $BT_ROOT — plugin cache incomplete.` STOP. |
+ | Status shows `hook_files=1/3` or `2/3` | Broken install (see the unit rule above). Repair with `upgrade` (or `install`) for that scope before anything else. |
+ | `registrations` below `3/3`, or `settings_refs=1` | Partly wired. `2/3` with only the `SubagentHandback` entry missing is an install from before that entry existed: auto-mode reports pass unsized. Re-run the merge via `upgrade`. |
+ | `agent-return-budget.mjs` registered in settings.json | Defect: it is a library. `upgrade`/`uninstall` strip that entry. |
+ | Custom thresholds where `passTokens >= fileTokens` | Reject before delegating and re-ask: the runbook's config block ABORTS on the inversion; installed by hand, it silently kills the compress tier. |
+ | `uninstall`/`purge` requested | Restate exactly what gets deleted (3 hook files, 3 settings entries, the `agent-return-state/` directory, a `settings.json`/hooks dir left empty, and for purge the config and an empty project `.claude/`), confirm once, then delegate. |
+ | Project config exists but is malformed JSON | Report it: the module skips it and silently uses the global config. Offer to rewrite. |
+ | Global, `CLAUDE_CONFIG_DIR` set, status shows another `config_dir` | The config is shared from `~/.claude`, and the runbook's global blocks act on `$CLAUDE_CONFIG_DIR` only: `upgrade` there aborts with "not installed in this scope". Run `upgrade` with `CLAUDE_CONFIG_DIR` empty (files + config in `~/.claude`), then the global copy and merge-settings blocks once per other config dir that registers the hooks. |
+ | `/brewtools:agent-deadline-setup` also installed | Both register `SubagentStop` and `PreToolUse` entries; hooks of one event run in parallel, and each skill's merge/strip touches only entries naming its own scripts. An agent challenged once here continues on its ORIGINAL deadline clock (the deadline cleanup stamps `stopped` and keeps the state; a stop stamp under 5 minutes old keeps the clock), and the deadline guard always allows `SubagentHandback`. An agent-deadline install from before 6.5 restarts the clock and, past its limit, also denies the hand-back: `upgrade` it too. |
---
## Smoke Test
- Verify the 4 assets exist and the hooks parse before delegating.
+ Before delegating, verify that the 4 assets exist and the hooks parse.
**EXECUTE** using Bash tool:
```bash
- SD="${CLAUDE_SKILL_DIR}"
- if [ -n "$SD" ] && [ -f "$SD/../../.claude-plugin/plugin.json" ]; then BT_ROOT=$(cd "$SD/../.." && pwd); else BT_ROOT=$(ls -d ~/.claude/plugins/cache/claude-brewcode/brewtools/*/ 2>/dev/null | sort -V | tail -1 | sed 's:/*$::'); fi
- [ -n "$BT_ROOT" ] || { echo "ERROR: cannot locate brewtools plugin root -- install/update brewtools first."; exit 1; }
+ BT_ROOT="${CLAUDE_PLUGIN_ROOT}"
A="$BT_ROOT/skills/agent-return-setup/assets"
test -d "$A" || { echo "❌ smoke FAILED — assets dir missing: $A"; exit 1; }
for f in agent-return-budget.mjs agent-return-contract.mjs agent-return-guard.mjs INSTALL.md; do
test -f "$A/$f" || { echo "❌ smoke FAILED — missing $f"; exit 1; }
done
node --check "$A/agent-return-budget.mjs" && \
node --check "$A/agent-return-contract.mjs" && \
node --check "$A/agent-return-guard.mjs" && \
echo "✅ smoke" || echo "❌ smoke FAILED — syntax error in a hook file"
```
- > **STOP if ❌** — do NOT delegate; reinstall/update brewtools first.
+ > **STOP if ❌** — do not delegate; reinstall/update brewtools first.
- `node --check` proves the files parse, nothing more. Behavioral verification (synthetic payloads for both tiers, the `stop_hook_active` brake, fail-open, the contract schema) is in the runbook's **Verify** section and is NOT run here.
+ `node --check` proves only that the files parse; behavioral checks are the runbook's **Verify** section and are not run here.
</instructions>