Skill Store 33

Agent Deadline Setup

Installs, configures or removes the agent-deadline hooks (soft wall-clock budget for subagents).

by kochetkov-ma · MIT

The skill

Installs, configures or removes the agent-deadline hooks (soft wall-clock budget for subagents). Triggers: agent-deadline, subagent timeout, agent time limit, дедлайн агента, таймаут саб-агента.

Agent Deadline

Two self-contained hooks (PreToolUse guard + SubagentStop cleanup) put a soft wall-clock budget on every subagent. This skill decides mode, scope and budget only; the brewcode:hook-creator agent does the file work from the runbook.

Claude Code has no wall-clock timeout for subagents. maxTurns stops a run at its turn cap; since 2.1.246 the caller receives a partial-marked result with a continuation hint, while unwritten analysis remains unrecoverable. These hooks kill nothing: they narrow what an over-budget agent can still call, so it finishes its report.

Prompt contract

$ARGUMENTS is one free-form RU/EN prompt; mode tokens and flags (--unattended) may sit anywhere in it.

  1. Mode: strip --unattended first: a flag, never a mode, scope or name (Modes, Unattended). Then 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. --unattended asks nothing: an open question there ends in RESULT skipped (Modes, Unattended).

Print once, before the first action (labels literal, values in the conversation language):

PLAN — brewtools:agent-deadline-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>

What the hooks do (informational — skill does NOT implement)

Hook Event Behavior
agent-deadline-guard.mjs PreToolUse (.*) tracks elapsed per agent_id and narrows the tools, table below
agent-deadline-cleanup.mjs SubagentStop stamps stopped on the agent's state file, never deletes it; prunes state by age
Elapsed Guard
80% of budget non-blocking additionalContext notice
100% permissionDecision:"deny" outside the finalization set: Read, Write, Edit, NotebookEdit, TodoWrite, TaskUpdate, SubagentHandback (the 7 its messages advertise) plus TaskCreate, unlisted by design
hardStopRatio x budget (config key, default 2, must be >1) hard stop: only Write, Edit, SubagentHandback pass, deny reason starts Time budget far exceeded. Stops an agent that loops inside the finalization set (re-reading files, rewriting todos)

SubagentHandback is always allowed: in auto mode a subagent reports through it, so a deny leaves the caller with no report. AskUserQuestion is denied on purpose: a subagent parked on a human answer is unbounded wall-clock time. Agent types in config readOnlyAgentTypes (default Explore, Plan) are told to return what they have now, every other agent to save to a file if the task expects one, then report. The payload has no tool inventory, so an unlisted custom read-only agent is still told to save.

State: <os.tmpdir()>/brewtools-agent-deadline/<session_id>/<agent_id>.json = { start, warned, expired, config, stopped? }, config = path of the governing config file (what uninstall/purge match on); pruned by age: stop stamps past 5 minutes, files and session dirs untouched for 24 h. SubagentStop fires several times per agent, so cleanup keeps the state and the guard decides on the same agent_id's next tool call (time-based: the payload does not tell a continued run from a resumed one):

Stop stamp age Clock When
under 5 minutes original clock continues, the pause counts stop blocked once by another guard (e.g. the agent-return size check), harness nudge, quick SendMessage resume: an over-budget agent can only finalize
5 minutes or more, or state already pruned fresh window from that call a later resume, or any pause that long

Honest limits (verified on CC 2.1.220, stop/resume on 2.1.296 — state these to the user, do not oversell)

Fact Consequence
Time is sampled only at tool-call boundaries An agent stuck inside one 25-min Bash call is not observed in between: a soft deadline, not a timeout. Cap long commands with BASH_MAX_TIMEOUT_MS.
Clock starts at the agent's first tool call, not at spawn Pre-tool thinking time is free.
Stop/resume is the table above A quick resume inherits a spent budget; a 5-minute pause earns a fresh one.
Main session vs subagent is discriminated by absence of agent_id/agent_type Main session = no-op, always.
agent_type for plugin agents (brewtools:text-optimizer vs text-optimizer) was not observed live A byAgentType key that does not match the real payload value silently falls back to defaultMinutes. Verify against a real payload before relying on an override.
Hook is fail-open Any error = the call passes through; the session never breaks.
Cost per tool call, paid by every call (matcher .*, needed to see SubagentHandback): median 58.3 ms, p90 62.5 ms, main-session no-op path 61.5 ms (measured: Apple M-series, Node v24.1.0, 30 runs) Node startup dominates; the guard adds up to 20 readFileSync: stdin payload, up to 16 project-config probes (walk from cwd to the filesystem root), up to 2 global configs, state file. A global install charges ~60 ms to every tool call of every session in every repo, including sessions that never spawn a subagent.

<instructions>

BT_ROOT Resolver (use in EVERY bash block)

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.

BT_ROOT="${CLAUDE_PLUGIN_ROOT}"
test -d "$BT_ROOT/skills/agent-deadline-setup/assets" || { echo "❌ FAILED — BT_ROOT invalid: $BT_ROOT"; exit 1; }

Asset paths (all under $BT_ROOT/skills/agent-deadline-setup/assets/):

  • INSTALL.md — the runbook, sole source of commands (install project/global, config shape, upgrade, disable/enable, uninstall, state removal, purge, verify): follow it, never re-derive them.
  • agent-deadline-guard.mjs, agent-deadline-cleanup.mjs — the two hook files that travel together

Global scope = the config dir in use, ${CLAUDE_CONFIG_DIR:-$HOME/.claude}. Write/Edit there hits a sensitive-path prompt and fails headless, so global operations use Bash only (cp/node/rm), per the runbook.

Opt-in: the hooks are not registered in brewtools/hooks/hooks.json, so the plugin alone does nothing until this skill runs.


Step 1 — STATUS FIRST, always

After the PLAN, probe status in every mode, before any mutation.

EXECUTE using Bash tool:

BT_ROOT="${CLAUDE_PLUGIN_ROOT}"
A="$BT_ROOT/skills/agent-deadline-setup/assets"
test -f "$A/INSTALL.md" && test -f "$A/agent-deadline-guard.mjs" && test -f "$A/agent-deadline-cleanup.mjs" || { echo "❌ FAILED — assets incomplete under BT_ROOT=$BT_ROOT"; exit 1; }
echo "ASSETS_DIR=$A"
echo "RUNBOOK=$A/INSTALL.md"
content_state() {
  ASSETS="$A" TARGET="${1}" node <<'NODE'
const fs=require("fs"), path=require("path");
const assets=process.env.ASSETS, target=process.env.TARGET;
const scripts=["agent-deadline-guard.mjs","agent-deadline-cleanup.mjs"];
const file=path.join(target,"agent-deadline.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{}
// config contract = CV of the guard, the one hook that parses the config; runbook prose never stales a config
const contractCV=cvOf(path.join(assets,"agent-deadline-guard.mjs"));
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||!contractCV||configCV!==contractCV) findings.push("agent-deadline.json:"+(configCV||"n/a")+"->"+(contractCV||"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||!contractCV||pairs.some(([installed,source])=>!installed||!source)) state="partial";
  else state=configCV!==contractCV||pairs.some(([installed,source])=>installed!==source)?"stale":"current";
}
console.log([configCV||"n/a",contractCV||"n/a",runbookCV||"n/a",state,findings.join(",")||"none"].join("|"));
NODE
}
# rc 0: the dir is HOME or the active config dir's parent - its .claude is a profile, never a project root.
is_profile_parent() {
  _pp=$(cd "${1}" 2>/dev/null && pwd -P) || return 1
  [ -n "${HOME:-}" ] && [ "$_pp" = "$(cd "$HOME" 2>/dev/null && pwd -P)" ] && return 0
  [ -n "${CLAUDE_CONFIG_DIR:-}" ] && [ "$_pp/.claude" = "$(cd "$CLAUDE_CONFIG_DIR" 2>/dev/null && pwd -P)" ]
}
claude_project_root() {
  PD="${CLAUDE_PROJECT_DIR}"   # substituted at load: the session start dir, as hooks get it
  if [ -n "$PD" ] && [ -d "$PD" ]; then
    printf '%s\n' "$PD"; return 0
  fi
  if r=$(git rev-parse --show-toplevel 2>/dev/null) && [ -n "$r" ]; then
    printf '%s\n' "$r"; return 0
  fi
  d=$PWD
  while [ "$d" != "/" ]; do
    if [ -d "$d/.git" ] || { [ -d "$d/.claude" ] && ! is_profile_parent "$d"; }; then printf '%s\n' "$d"; return 0; fi
    d=$(dirname "$d")
  done
  printf '%s\n' "$PWD"; return 1
}
if ROOT=$(claude_project_root); then ROOT_OK=yes; else ROOT_OK=no; fi
echo "project_root=$ROOT root_resolved=$ROOT_OK"
for S in "$ROOT/.claude:project" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}:global"; do
  D="${S%%:*}"; N="${S##*:}"
  G=no; [ -f "$D/hooks/agent-deadline-guard.mjs" ] && G=yes
  C=no; [ -f "$D/hooks/agent-deadline-cleanup.mjs" ] && C=yes
  REFS=$(SETTINGS="$D/settings.json" SCOPE="$N" HOOKS_DIR="$D/hooks" node <<'NODE'
const fs=require("fs"), path=require("path");
const f=process.env.SETTINGS, scope=process.env.SCOPE, dir=process.env.HOOKS_DIR;
const marks=["agent-deadline-guard.mjs","agent-deadline-cleanup.mjs"];
let s={};
let settingsValid=true;
try{
  if(fs.existsSync(f)&&fs.readFileSync(f,"utf8").trim()) s=JSON.parse(fs.readFileSync(f,"utf8"));
  if(s===null||typeof s!=="object"||Array.isArray(s)) settingsValid=false;
}catch{ settingsValid=false; s={}; }
// token split on purpose: the dollar-brace spelling would be substituted when this skill loads
const expected=marks.map(m=>scope==="project"?"$"+"{CLAUDE_PROJECT_DIR}/.claude/hooks/"+m:path.join(dir,m));
const specs={
  "agent-deadline-guard.mjs":{event:"PreToolUse",matcher:".*",arg:expected[0],timeout:5},
  "agent-deadline-cleanup.mjs":{event:"SubagentStop",matcher:null,arg:expected[1],timeout:3},
};
const argsOf=h=>Array.isArray(h&&h.args)?h.args.filter(a=>typeof a==="string"):[];
const ownedScript=h=>{
  let body="";
  try{ body=JSON.stringify(h); }catch{}
  return marks.find(m=>body.includes(m));
};
const matcherIs=(entry,matcher)=>matcher===null?!Object.prototype.hasOwnProperty.call(entry,"matcher"):entry.matcher===matcher;
const exactKeys=h=>h&&typeof h==="object"&&!Array.isArray(h)&&Object.keys(h).sort().join(",")==="args,command,timeout,type";
const wired={"agent-deadline-guard.mjs":0,"agent-deadline-cleanup.mjs":0};
const wrapped={"agent-deadline-guard.mjs":0,"agent-deadline-cleanup.mjs":0};
// First word of `command` != node: the project's own launcher around the hook, report-only.
const launcher=h=>{ const m=/^\s*(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(typeof h.command==="string"?h.command:""); return m?String(m[1]??m[2]??m[3]).split(/[\\/]/).pop():""; };
const isWrapped=h=>typeof h.command==="string"&&!/^node(\.exe)?$/i.test(launcher(h));
let legacy=0;
for(const [event,entries] of Object.entries((s&&s.hooks)||{})){
  if(!Array.isArray(entries)) continue;
  for(const entry of entries){
    if(!entry||typeof entry!=="object"||!Array.isArray(entry.hooks)) continue;
    for(const handler of entry.hooks){
      const script=ownedScript(handler);
      if(!script) continue;
      if(isWrapped(handler)){ wrapped[script]+=1; continue; }
      const spec=specs[script];
      const exact=settingsValid&&event===spec.event&&matcherIs(entry,spec.matcher)&&exactKeys(handler)&&handler.type==="command"&&handler.command==="node"&&argsOf(handler).length===1&&argsOf(handler)[0]===spec.arg&&handler.timeout===spec.timeout;
      if(exact) wired[script]+=1; else legacy+=1;
    }
  }
}
console.log(wired[marks[0]]+"|"+wired[marks[1]]+"|"+legacy+"|"+(settingsValid?"yes":"no")+"|"+wrapped[marks[0]]+"|"+wrapped[marks[1]]);
NODE
  )
  GR=${REFS%%|*}; REST=${REFS#*|}; CR=${REST%%|*}; REST=${REST#*|}; LEGACY=${REST%%|*}; REST=${REST#*|}; VALID=${REST%%|*}; REST=${REST#*|}; GW=${REST%%|*}; CW=${REFS##*|}
  CFG=none; [ -s "$D/agent-deadline.json" ] && CFG=$(tr -d '\n ' < "$D/agent-deadline.json"); CFG=${CFG:-none}
  EN=n/a; case "$CFG" in *'"enabled":true'*) EN=true;; *'"enabled":false'*) EN=false;; esac
  CV=$({ jq -r '.version // empty' "$D/agent-deadline.json" 2>/dev/null || true; }); CV=${CV:-n/a}
  IFS='|' read -r CCV KCV RCV CONTENT FINDINGS <<< "$(content_state "$D")"
  echo "$N: guard=$G cleanup=$C guard_refs=$GR cleanup_refs=$CR legacy_refs=$LEGACY settings_valid=$VALID guard_wrapped=$GW cleanup_wrapped=$CW enabled=$EN config_version=$CV config_content_version=$CCV contract_content_version=$KCV runbook_content_version=$RCV content_state=$CONTENT content_findings=$FINDINGS config=$CFG"
  if [ "$N" = project ]; then P_WIRED=$((GR+CR+LEGACY+GW+CW)); else G_WIRED=$((GR+CR+LEGACY+GW+CW)); fi
done
# governing config, resolved like the guard's loadConfig(cwd): from the payload cwd (= this shell's
# $PWD, not project_root) up to the nearest valid project config, then global dirs
GOV=$(START="$PWD" node <<'NODE'
const fs=require("fs"), path=require("path"), os=require("os");
const name="agent-deadline.json", env=(process.env.CLAUDE_CONFIG_DIR||"").trim();
const globals=[...new Set([env?path.resolve(env):null,path.join(os.homedir(),".claude")].filter(Boolean))];
const ok=f=>{try{const c=JSON.parse(fs.readFileSync(f,"utf8"));return !!c&&typeof c==="object"&&!Array.isArray(c);}catch{return false;}};
let dir=path.resolve(process.env.START), hit="none", scope="none";
for(let i=0;i<16;i++){
  if(globals.includes(path.join(dir,".claude"))) break;
  const f=path.join(dir,".claude",name);
  if(ok(f)){ hit=f; scope="project"; break; }
  const up=path.dirname(dir); if(up===dir) break; dir=up;
}
if(scope==="none") for(const g of globals) if(ok(path.join(g,name))){ hit=path.join(g,name); scope="global"; break; }
console.log(hit+"|"+scope);
NODE
)
GOV_SCOPE=${GOV##*|}; ORPHAN=no
[ "$GOV_SCOPE" = project ] && [ "${P_WIRED:-0}" = 0 ] && [ "${G_WIRED:-0}" != 0 ] && ORPHAN=yes
echo "governing_config=${GOV%|*} governing_scope=$GOV_SCOPE orphan_config=$ORPHAN"
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
project_root / root_resolved CLAUDE_PROJECT_DIR (session start dir, as hooks get it), then git toplevel, then an owning .git/.claude ancestor; root_resolved=no = read-only fallback to $PWD
guard_refs / cleanup_refs handlers matching the exact tuple, per script. Guard = PreToolUse + .* + timeout 5, cleanup = SubagentStop + no matcher + timeout 3, both command/node/sole portable arg; global args are the expanded <global dir>/hooks/<script> path. Each must be exactly 1: more is a duplicate and non-effective, and two guards never substitute for a missing cleanup
legacy_refs owned handlers that differ from the exact tuple: absolute project paths, swapped events, wrong matchers/types/commands/timeouts, extra args/keys, malformed handler values; any nonzero value requires migration
settings_valid yes only when settings are absent/empty or parse as a JSON object; anything else is non-effective
guard_wrapped / cleanup_wrapped handlers naming that script (in command or args) launched by something other than node: the project's own launcher, e.g. "$CLAUDE_PROJECT_DIR/.claude/scripts/preflight.py" exec node -- ".../agent-deadline-guard.mjs". User-owned, report-only: never legacy, never rewritten, and install/upgrade add no unwrapped copy beside them
enabled true/false parsed from the config; n/a = no config or no enabled key
config_version release provenance, shown beside plugin_version; never a staleness signal
config_content_version / contract_content_version installed config CV vs the header CV of agent-deadline-guard.mjs (the config contract: the one hook that parses the config); each installed hook vs its asset header the same way
runbook_content_version the INSTALL.md header CV; informational only: a runbook-only change never stales an installed config
content_state current: all CV pairs match (config vs contract, each hook vs its asset); 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
content_findings comma-separated artifact:installed-CV->source-CV for every mismatched/missing pair, or none; n/a = missing/malformed metadata, never a guessed version
governing_config / governing_scope the one config every wired guard reads, resolved like loadConfig(cwd): nearest valid (JSON object) .claude/agent-deadline.json from the shell cwd $PWD up (the guard climbs from the payload cwd: a config below or above project_root counts), at most 16 levels, stopping at a global dir; then $CLAUDE_CONFIG_DIR, then ~/.claude; none = no valid config anywhere
orphan_config yes = the governing config is project-side, the project has no owned registration (guard_refs+cleanup_refs+legacy_refs+both *_wrapped = 0) and the global scope has one: the global guard obeys the project file (typically left by uninstall project)

Print status as this table:

Scope Hook files guard refs cleanup refs legacy refs wrapped settings valid Config Config ver Content state Effective

Config metadata (the four standard JSON keys)

Every mode that writes agent-deadline.json (install, upgrade, enable, disable) leaves these four keys beside the behavior keys (no doc_type: .md frontmatter only):

{ "version": "{PLUGIN_VERSION}", "content_version": "<agent-deadline-guard.mjs header>", "generated_by": "brewtools:agent-deadline-setup", "last_updated": "{LAST_UPDATED}" }

The runbook resolves content_version from the brewcode-meta: header of agent-deadline-guard.mjs beside $RUNBOOK and aborts if missing; never pass or hardcode it. Resolve version and last_updated here. EXECUTE using Bash tool:

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

The four keys are inert at runtime: the guard's loadConfig() reads only enabled (gate cfg.enabled !== true -> off), defaultMinutes, byAgentType, hardStopRatio, readOnlyAgentTypes.

Content state and enabled/wiring are separate signals. Offer upgrade for stale or partial, naming the mismatched/missing metadata or artifact, never from a release-version difference alone, and never for a scope with orphan_config=yes (below). Status never edits files or settings.

Effective = guard=yes cleanup=yes guard_refs=1 cleanup_refs=1 legacy_refs=0 settings_valid=yes enabled=true; a script counted under *_wrapped instead of *_refs stands in for its 1 (custom launcher: say "registered through a custom launcher (report-only)" and never offer install/upgrade for that registration). Anything else, including duplicate-one/missing-other registrations or a malformed owned handler, is not effective: say so; never report a half-state as installed. A malformed project config is skipped (the guard silently climbs to an ancestor config, else the global one; governing_config names the winner): report it and offer to rewrite.

orphan_config=yes is a finding, printed under the table: <governing_config> is GOVERNING: the global guard reads the nearest project config first, so its enabled and budget override the global config. Fix: /brewtools:agent-deadline-setup purge project (deletes it and its state; the global config then governs). Keeping it on purpose is valid: uninstall project keeps it and warns. Never offer upgrade for it: upgrade re-wires the project hooks and undoes the uninstall.

Early exit

Everything the user could want is already installed and the intent is not explicit (no argument, or vague like "агент-дедлайн") -> print the status, list the available operations (upgrade, enable, disable, change budget, uninstall, purge, install for the other scope) and stop: no re-install, no chain of questions.

Step 2 — Decide MODE

Mode EN keywords RU keywords Mutates?
status (empty), status статус, проверь, что стоит no
install install, set up, bare number of minutes поставь, установи, включи дедлайн yes
upgrade upgrade, update, refresh обнови, перевыстави, после обновления плагина yes
enable enable включи обратно, верни yes
disable disable выключи, отключи, паузу yes
uninstall uninstall убери, сними, удали хук yes
purge purge, wipe, remove everything вычисти всё, удали полностью, убери совсем, снеси yes, destructive

Ties resolve per Prompt contract rule 1.

Step 3 — Explain pending changes before questions

Add concrete state/action details to the PLAN without repeating its block, e.g.:

Current state: not installed anywhere. Plan: copy the 2 hook files into <repo>/.claude/hooks/, write <repo>/.claude/agent-deadline.json, merge PreToolUse + SubagentStop entries into <repo>/.claude/settings.json. I need 2 answers first: scope and budget.

Step 4 — Ask ONLY what is missing (AskUserQuestion)

# Question Options Default
1 Scope — this project or all projects? Project (<repo>/.claude) / Global (the config dir in use, ~/.claude by default) / Both none — NEVER guess, always ask unless explicit
2 Deadline budget per subagent? 20 min (Recommended) / 30 min / 45 min / 10 min 20
3 Per-agent-type overrides? Uniform limit for all agents (Recommended) / Define overrides uniform, byAgentType: {}

Global scope (question 1 or the arguments): state the per-call cost (last row of Honest limits) before writing anything, in the option description or the plan, never only in the final report.

Ask question 3 only if the user raised per-type limits (then first give them the agent_type row of Honest limits); otherwise install uniform and mention in the final report that overrides exist.

For disable/enable/uninstall/purge only question 1 applies, and only when status shows the feature in more than one scope.

Step 5 — Act on the resolved PLAN

Resolve MODE/SCOPE, exact paths, defaultMinutes and the settings.json entries before any mutation. For uninstall/purge, list the exact deletions (this scope's state files included) and confirm once; for uninstall project while the global scope has registrations, say the kept project config will keep GOVERNING the global guard, and offer purge project in the same confirmation. Status and early exit report the table and stop; their DO: is "read state, report".

Delegation

One mode × one scope (2 asset files + one settings.json + one config) = one hook-creator spawn; both scopes = two spawns in one message.

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, substituting MODE, SCOPE, MINUTES, OVERRIDES, HARD_STOP_RATIO, RUNBOOK, ASSETS_DIR, PROJECT_ROOT, PLUGIN_VERSION, LAST_UPDATED (Steps 1-4, Config-metadata block) into both the CONTEXT block and the literal export line: the runbook reads MINUTES/OVERRIDES/RUNBOOK from process.env, and a missing MINUTES aborts (no built-in 20 fallback). PROJECT_ROOT = Step 1 project_root (the runbook's root block takes CLAUDE_PROJECT_DIR first, which the Bash tool lacks); with root_resolved=no, drop it from the export line. Afterwards check that the reported config carries the chosen number:

Agent(subagent_type="brewcode:hook-creator", prompt="
GOAL: the user wants the agent-deadline hooks MODE-ed for SCOPE. Two hooks (PreToolUse
guard + SubagentStop cleanup) impose a soft wall-clock budget on subagents: warn at 80%,
deny non-finalization tools past 100%. Runtime behavior lives entirely in the hook files
and in agent-deadline.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 2 assets under ASSETS_DIR, the target .claude/ dir, its settings.json,
its agent-deadline.json. Out — everything else. Project scope: Write/Edit are fine.
Global scope (the config dir in use: ~/.claude unless CLAUDE_CONFIG_DIR is set): BASH ONLY
(cp + node + rm), never Write/Edit — a sensitive path that prompts and fails 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)
  MINUTES = MINUTES (defaultMinutes 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)
  OVERRIDES = OVERRIDES (byAgentType JSON object, {} unless the user asked otherwise)
  HARD_STOP_RATIO = HARD_STOP_RATIO (multiple of the budget after which the allowance
    shrinks to Write/Edit and the report call; omit entirely to keep the hook default of 2,
    must be > 1)
  RUNBOOK = RUNBOOK (absolute path to assets/INSTALL.md)
  ASSETS_DIR = ASSETS_DIR (absolute path to the assets source dir — copy the 2 hook files FROM here)
  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' MINUTES='MINUTES' OVERRIDES='OVERRIDES' HARD_STOP_RATIO='HARD_STOP_RATIO' PLUGIN_VERSION='PLUGIN_VERSION' LAST_UPDATED='LAST_UPDATED' CLAUDE_PROJECT_DIR='PROJECT_ROOT'
  Then verify before writing anything:
    echo \"MINUTES=\$MINUTES OVERRIDES=\$OVERRIDES HARD_STOP_RATIO=\$HARD_STOP_RATIO RUNBOOK=\$RUNBOOK PV=\$PLUGIN_VERSION LU=\$LAST_UPDATED PD=\$CLAUDE_PROJECT_DIR\"
  If MINUTES prints empty, STOP and report — the config block ABORTS on an empty MINUTES
  by design; re-export it rather than hardcoding a number.
  Drop HARD_STOP_RATIO from the export line when the user did not set it.
  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' (+ 'STATE REMOVAL'), 'PURGE'. Merge strips owned handlers individually while
  preserving foreign co-handlers, then appends exactly one handler per script. Project args use
  the runbook's portable literal (dollar-brace CLAUDE_PROJECT_DIR, then /.claude/hooks/<script>)
  and migrate legacy absolute checkout args; global args stay expanded absolute paths. Both
  paths are idempotent.
  Upgrade = the 'UPGRADE' section: it reads MINUTES back out of the existing config (OVERRIDES
  stays {} and HARD_STOP_RATIO unset: the config block keeps the stored byAgentType and
  hardStopRatio), then replays the copy + config + merge blocks for SCOPE.
  Uninstall = two steps: strip handlers by those two basenames, drop empty entries/event arrays and
  delete the 2 files, then run 'STATE REMOVAL' with this scope's CFG; KEEP the config. Step 1
  prints a WARNING: line when the kept project config will govern the global guard: quote it
  verbatim in your report. Purge =
  uninstall (both steps) + delete this scope's config + 'STATE REMOVAL' again. State removal
  deletes only state files recorded for CFG (plus files untouched for 24 h); never remove the
  shared brewtools-agent-deadline tmp root by hand.
  METADATA: every mode that WRITES the config (install, upgrade, enable, disable) must leave
  these four keys in agent-deadline.json: version=\$PLUGIN_VERSION,
  content_version=<read by the runbook from the agent-deadline-guard.mjs brewcode-meta header; never hardcoded>,
  generated_by=\"brewtools:agent-deadline-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; Claude Code watches settings.json and
  applies your write to the running session, so a malformed merge breaks hooks there instead
  of failing here — report the exact paths you touched so they can be checked.
DONE: report the settings.json path, the hooks dir, the config path with its final contents,
  and the runbook 'Verify' output if you ran it. The reported config MUST show
  defaultMinutes = MINUTES — a 20 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>.
")

Step 6 — Final status

Re-run the Step 1 status block; print the refreshed table plus:

  • what changed (files, settings.json, config values, state files removed), and any WARNING: kept ... still GOVERNS line from an uninstall, verbatim, with the purge project fix;
  • hook wiring changes (install/upgrade/uninstall/purge) apply live, no restart (Claude Code watches settings files; /hooks confirms); /reload-plugins does not apply to plain settings.json hooks;
  • config value changes are read live, no restart;
  • the soft-deadline caveat: time is sampled at tool-call boundaries only; pair with BASH_MAX_TIMEOUT_MS for long single commands.

Modes

Mode Effect Files settings.json Config State
status report only — — — —
install wire + configure copied entries merged written —
upgrade re-emit from the current plugin version re-copied entries re-merged values preserved kept
enable enabled:true kept kept edited kept
disable enabled:false kept kept edited kept
uninstall unwire (runbook UNINSTALL, 2 steps) deleted entries stripped kept this scope's removed
purge uninstall + config (runbook PURGE, 3 steps) deleted entries stripped deleted this scope's removed

State removal = the runbook's ## STATE REMOVAL block (uninstall step 2, purge step 3): deletes state files whose config equals this scope's config path, plus any untouched for 24 h (pre-6.5 state has no config and ages out). The brewtools-agent-deadline root (shared by every project and the global install) and a session dir go only when empty: wiping the root resets every live agent's clock.

upgrade asks nothing and keeps defaultMinutes, byAgentType, hardStopRatio and a disabled state. Not installed in that scope -> install: ask scope and budget.

Unattended (--unattended anywhere in the prompt; set by /brewcode:setup-status upgrade).

  1. A flag, never a mode or name: strip it first. Any resolved mode other than upgrade -> no write, RESULT skipped <id> cv=?->? pending=0 note="not-upgrade".
  2. No AskUserQuestion at all. Every gate in this section takes its decline branch; a declined gate is pending, never applied.
  3. A precondition this section would ask about (not installed, parked, custom/wrapped registration, orphan config, user-modified managed file, missing tokenizer) -> no write, RESULT skipped <id> cv=?->? pending=0 note="<reason>".
  4. Write only what this section already writes. No git commit, push, stash or reset; scratch only in a private mktemp -d.
  5. Last line, exactly once: RESULT <ok|partial|skipped|failed> <id> cv=<before>-><after> pending=<n> note="<one line>", <id> = <plugin>:<skill> [ scope=<project|global>| team=<name>], ? = unknown version.

Here <id> = brewtools:agent-deadline-setup scope=<project|global>; setup-status passes the scope:

  • The prompt names exactly one of project/global, else RESULT skipped brewtools:agent-deadline-setup cv=?->? pending=0 note="no scope token".
  • Step 1 decides for that scope, first hit wins, before the Smoke Test: orphan_config=yes -> skipped, note orphan config: purge project or keep it; config=none -> skipped, note not installed; guard=no, cleanup=no and every *_refs/*_wrapped count 0 (uninstall keeps the config; the replay would undo it) -> skipped, note not installed (uninstalled, config kept); guard_wrapped or cleanup_wrapped not 0 -> skipped, note custom launcher; settings_valid=no -> skipped, note settings.json invalid.
  • Otherwise Steps 3-4 are skipped: Smoke Test, then the one hook-creator spawn with MODE=upgrade. The read-back ABORT: (UPGRADE step 1, nothing written yet) -> skipped, its text as note; an ABORT: from a later block (the hook copy already ran), a ❌ in Step 1 or the Smoke Test, or a failed spawn -> failed.
  • Step 6 status for that scope: content_state=current -> ok, else failed. cv = config_content_version before -> after (n/a -> ?); pending=0 (this upgrade has no gate); a kept enabled=false is named in note.

Smoke Test

Before delegating, verify the 3 assets exist and the hooks parse. EXECUTE using Bash tool:

BT_ROOT="${CLAUDE_PLUGIN_ROOT}"
A="$BT_ROOT/skills/agent-deadline-setup/assets"
test -d "$A" || { echo "❌ smoke FAILED — assets dir missing: $A"; exit 1; }
for f in agent-deadline-guard.mjs agent-deadline-cleanup.mjs INSTALL.md; do
  test -f "$A/$f" || { echo "❌ smoke FAILED — missing $f"; exit 1; }
done
node --check "$A/agent-deadline-guard.mjs" && \
node --check "$A/agent-deadline-cleanup.mjs" && \
echo "✅ smoke" || echo "❌ smoke FAILED — syntax error in a hook file"

STOP if ❌ — do not delegate; reinstall/update brewtools first.

node --check proves parsing only; behavioral checks (synthetic payloads, forced deadline) are the runbook's Verify section, not run here.

</instructions>

Use this skill

Connect your agent to the Skill Store MCP server and ask for it by ref: kochetkov-ma/agent-deadline-setup-77762548820.

Download SKILL.md · Markdown version

Details

Publisher
kochetkov-ma
Category
AI & Agents › Orchestration & multi-agent
License
MIT
Updated
2026-10-17

References

More from kochetkov-ma