---
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.

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.

## Prompt contract

Position 1 of `$ARGUMENTS` is a **free-form prompt** (RU/EN) — modes and flags are optional and may
follow in any order. Nobody types keys: resolve mode + scope FROM the prompt.

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.

Then print this block ONCE, before the first action:

```
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 |

Tiers, `t = Math.ceil(last_assistant_message.length / 4)`:

| 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 |

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:`.

**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.

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.

## 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.

## 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 |

<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:

```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; }
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

> 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.

> Opt-in by design: these hooks are NOT registered in `brewtools/hooks/hooks.json`, so installing the plugin does nothing until this skill runs.

---

## Step 1 — STATUS FIRST, always

Run this before anything else, in EVERY mode. Never install, re-install or remove blind.

**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; }
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"
for S in "$PWD/.claude:project" "$HOME/.claude:global"; do
  D="${S%%:*}"; N="${S##*:}"
  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}
  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}
  echo "$N: hook_files=$F/3 settings_refs=$W enabled=$EN pass=$PT file=$FT config_version=$CV 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:

| 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` | the config's `version` key vs `plugin_version` on the last line. Different = the config was written by an older brewtools and may predate a shape change -> offer `upgrade`. `n/a` on either side = unknown, NOT "current" |
| `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.

Read the output into a state table and PRINT it to the user:

| Scope | Hook files | settings.json wired | pass/file | Config ver | Stale | Effective |
|-------|-----------|---------------------|-----------|------------|-------|-----------|

### Config metadata (the three standard JSON keys)

Every mode that writes `agent-return.json` (`install`, `upgrade`, `enable`, `disable`) leaves these three keys in it alongside the behavior keys. `doc_type` is a `.md`-frontmatter field only and never appears in a JSON carrier:

```json
{ "version": "{PLUGIN_VERSION}", "generated_by": "brewtools:agent-return-setup", "last_updated": "{LAST_UPDATED}" }
```

Resolve `version` and `last_updated` — never hardcode either. **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; }
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)"
```

> **Why the bare form.** `CLAUDE_SKILL_DIR` is a TEXT SUBSTITUTION on the skill prompt, not an env var: CC 2.1.226 rewrites only the EXACT dollar-brace literal `{CLAUDE_SKILL_DIR}` (`replace(/\$\{CLAUDE_SKILL_DIR\}/g, dirname(skillPath))` and a string-pattern `replaceAll`). A brace-modifier form such as `:-fallback` inside the braces is therefore NOT matched, reaches the shell verbatim, and its fallback ALWAYS wins. `CLAUDE_PLUGIN_ROOT` is a real env var but is exported only to hook processes and MCP servers -- never to a skill's Bash tool -- so it is ALWAYS empty here. The skill dir is correct in a cache install AND in a `--plugin-dir` dev run; the cache glob below it is a last-resort fallback only, and it would name 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 |

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.

### 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.

## Step 2 — Decide MODE

Read `$ARGUMENTS`. Default when there are NO arguments at all = **status** if installed anywhere, else **install**.

| 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.

## Step 3 — State the plan BEFORE asking anything

Plain text, before any question:

> 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.

## Step 4 — Ask ONLY what is missing (`AskUserQuestion`)

Skip any question already answered by `$ARGUMENTS` 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 |

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.

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.

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 — Print the PLAN block, then act

Print the `## Prompt contract` PLAN block, filled with the resolved MODE/SCOPE (exact paths,
exact `passTokens`/`fileTokens`, exact settings.json entries) — then proceed. For `uninstall`/`purge`
list exactly which files are deleted and confirm once. Status (early exit or explicit `status`
mode) prints the SAME block, `DO:` reduced to "read state, report", immediately before the table.

### Delegation

A big task handed to one agent = an agent gone for an hour: unobservable, uncorrectable, drifting. 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 spawn prompt MUST carry:

| Field | Content |
|-------|---------|
| GOAL | the overall task and why it exists |
| ROLE | what this agent owns; what it must NOT touch |
| SCOPE | exact paths/commands in bounds + explicit out-of-bounds |
| CONTEXT | what is already done, what runs in parallel — trimmed to what THIS agent needs |
| CONSUMER | who uses the result next and the shape it must fit |
| DONE | acceptance criteria + the exact report 