Skip to content

feat(ways): standing delegation authorization for the AgentTool gate (ADR-175) - #432

Merged
aaronsb merged 3 commits into
mainfrom
feat/delegation-authorization
Jul 31, 2026
Merged

feat(ways): standing delegation authorization for the AgentTool gate (ADR-175)#432
aaronsb merged 3 commits into
mainfrom
feat/delegation-authorization

Conversation

@aaronsb

@aaronsb aaronsb commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Problem

Claude Code 2.1.219+ injects a system-prompt section (heron_brook) on Opus 5 carrying a
single constant, emitted as one block:

Do not call the AgentTool unless the user requested it
Do not use workflows or deep-research unless the user requested it

Gate: the opus_5_prompt_bundle model capability plus killswitch tengu_fennel_godwit,
currently false. Confirmed live from inside two sessions. Upstream:
anthropics/claude-code#80988,
open, no staff reply.

No opt-out exists. CLAUDE_INTERNAL_FC_OVERRIDES is dead code in 2.1.220 (unconditional
return before the env read), and DISABLE_GROWTHBOOK=1 makes it worse since the
killswitch defaults false. Flipping the killswitch is all-or-nothing: the same capability
gates five other sections including delivering_work_max and overcorrection.

The constant carves out no exception by agent type, so code-reviewer is suppressed
exactly like Explore. The operator has to name delegation explicitly every time —
including for procedures whose own steps call for it.

Approach

Evidence, not override. The gate asks a permission question, and its condition is
satisfiable in advance and in writing by the operator. An "ignore the above" countermand
fails on its own terms and wins only on recency, since way text and CLAUDE.md share the
operator's authorship.

A second gate (subagent_steer_delegation, arm counter_steer) asks a cost/benefit
question instead. It is answered by naming the payoff, not by overriding.

Changes

Tier 1 — doctrine in hooks/ways/meta/subagents/subagents.md: invocation is
authorization, plus a payoff clause for the cost/benefit gate.

Tier 2 — operative clause inline at each site that actually directs a delegation:

Site Where
skills/merge/SKILL.md the review gate, beside the Agent(...) dispatch
hooks/ways/softwaredev/delivery/merge/merge.md the four-square classification
hooks/ways/research/research.md the Gather fan-out step

Site-level rather than widening subagents.md's pattern. Reaching "let's merge this"
by regex needs common-word alternations (merge, review) — exactly the noise ADR-155 §5
exists to veto. A skill is read at invocation, so the clause is present whenever delegation
is about to happen with no dependence on a way firing.

delivery/branching was considered and declined: it scores high on merge-shaped queries but
directs no delegation, so placing an authorization there is trigger-chasing under a
different filename.

Scope

AgentTool only. Workflows and deep-research are the second line of the same constant
and stay gated — proposed and discussed, never invoked unprompted. Authorization is also
conditional: ad-hoc delegation outside an invoked procedure still asks first.

Also

subagents.md documented the tool as Task; the model-facing name is Agent. The Task
matchers in settings.json are the harness event namespace and are deliberately unchanged —
renaming them would break subagent way injection.

Verification

  • ways lint hooks/ways --check — 137 files, 0 errors, 0 warnings
  • adr lint — 0 errors (1 pre-existing warning on ADR-127, unrelated)

Behavioural acceptance is only observable in live sessions; it can't be verified from inside
the repo. Noted as a limitation in the ADR.

Known limitation

counter_steer was not observed in either sampled session, but both samples share one
account and GrowthBook buckets on stable user attributes, so they land the same arm by
construction. That gate is untested, not absent — §2 of the way text is written
defensively for it.

Implements ADR-175.

aaronsb added 2 commits July 31, 2026 00:52
…(ADR-175)

Claude Code 2.1.219+ injects a system-prompt constant on Opus 5 gating
AgentTool, workflows, and deep-research behind "unless the user requested
it". There is no opt-out: CLAUDE_INTERNAL_FC_OVERRIDES is dead code in
2.1.220, and the tengu_fennel_godwit killswitch is all-or-nothing across a
capability that also carries delivering_work_max and overcorrection.

Answer the gate on its own terms rather than overriding it. It asks a
permission question whose condition the operator can satisfy in advance.

Two tiers:

  - Doctrine in meta/subagents — invocation is authorization, plus a
    payoff clause answering the separately-gated counter_steer nudge.
  - Operative clause inline at each site that actually directs a
    delegation: skills/merge review gate, delivery/merge four-square,
    research Gather fan-out.

Site-level placement rather than widening subagents.md's pattern: the
tokens that would reach "let's merge this" are common-word alternations
that ADR-155 section 5 exists to veto, and a skill is read at invocation
so the clause is present with no dependence on a way firing.

Scope is AgentTool only. Workflows and deep-research stay gated.

Also corrects subagents.md documenting the tool as Task; the model-facing
name is Agent. The Task matchers in settings.json are the harness event
namespace and are deliberately left alone.
Peer review on #432 caught /develop named in the authorization list while
carrying no tier-2 clause. It spawns nothing — its own body calls it "a
router, not an orchestrator" — but it holds Agent in allowed-tools, so a
blanket grant would read as licence to spawn agents for stage work and
contradict the skill's framing.

Drop it from the list, and state the invariant that made the mistake
possible: tier 1 names only procedures carrying a clause at their own
dispatch site, and a router that sequences skills grants nothing.

Record the drift as fail-closed in ADR-175 — a site without a clause
grants nothing, so the model asks, which is the pre-change behaviour.
@aaronsb

aaronsb commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Code Review — ADR-175 standing delegation authorization

Reviewed at 3d9f140 (the branch moved mid-review; 4a1d68b + the /develop fix commit are both covered below). 200-ish lines added across two tiers plus one ADR — thorough-review tier.

Verification of the technical claims

scope: agent is main-loop delivery — confirmed. tools/ways-cli/src/cmd/scan/mod.rs:386-395 drops any candidate on the task surface whose scope lacks subagent (or teammate, when a team name marks the dispatch), and contains() is a substring test so agent does not slip through. tools/ways-cli/src/session.rs:474-495 resolves the prompt surface to agent for every non-teammate session and matches on exact comma-split equality. ADR-175:61-63 is right. One imprecision worth a word: the task filter is subagent or teammate (mod.rs:389-393), not subagent alone.

ADR-155 §5 — half-verified, and the Alternatives entry overstates it. review is literally in COMMON_WORDS (tools/ways-cli/src/cmd/lint/pattern.rs:51) and is flagged even when anchored — there is a regression test for exactly that at pattern.rs:345. merge is not in the list, and at five characters it clears SHORT_ALTERNATION_FLOOR (pattern.rs:33; the rule is char_len < 5), so a bare merge alternation passes lint today. More directly: hooks/ways/softwaredev/delivery/merge/merge.md:4 already ships merge (this|it|the pr) — a lint-clean, phrase-anchored trigger that matches the exact string ADR-175:72 says nothing reaches ("let's merge this"). pattern_keep: is a documented escape hatch for a measured keep on top of that (hooks/ways/meta/knowledge/authoring/authoring.md:55).

So ADR-175:75-76 and :139-141 claim more than the code supports. The conclusion survives on the other argument, which is the stronger one anyway: a skill is read at invocation with no dependence on the scanner, and reproducing another way's trigger surface inside subagents.md would put two ways on one query. Lead with that; soften the lint claim to name review only.

Upstream reference checks out. anthropics/claude-code#80988 is open, 11 comments, every one author_association: NONE — "no staff response" holds. The eleven independent reporters are better Context evidence than the two same-account samples and cost one sentence to cite.

Repro of the stated verification. ways lint hooks/ways --check → 137 files, 0 errors, 0 warnings. No re-embed is needed and none was done, correctly: the matching alias is description + vocabulary (authoring.md:62) and neither changed, so body-only edits move no coordinate.

Scope discipline — holds

Nothing in the diff loosens workflows or deep-research. Both new mentions re-gate them explicitly (subagents.md:83-85, research.md:19), and the Tier 2 clauses in merge.md and skills/merge/SKILL.md reach the reviewer dispatch only. settings.json is untouched, so the Task matchers at lines 226, 235, 270 and 285 stand — right call, they are the harness event namespace and renaming them would break subagent way injection.

One adjacent note, outside this PR: skills/ways-localize/SKILL.md:74-75 recommends "a good Workflow fan-out when the count is large." It is a recommendation rather than an authorization and predates this change, but it is the one place in the corpus a reader could mistake for one now that a delegation authorization exists nearby.

Blocking

1. github.md:24 contradicts the new doctrine on the same query.

hooks/ways/softwaredev/delivery/github/github.md:24 reads "At minimum offer a code-reviewer subagent." Its pattern (github.md:4) includes merge.?(it|this)|ship.?(it|this|the)|land.?(it|this)|review.?(pr|comment), and its scope is agent, subagent — so it fires on precisely the "let's merge this" moment ADR-175:72 says nothing reaches, alongside delivery/merge, which now says "dispatch the reviewer rather than re-requesting permission to" (merge.md:34-37).

Two ways can therefore land in one window telling the model offer and don't re-ask. Whichever is read last wins, which is the recency contest the Decision explicitly declines to enter (ADR-175:55-57). The fix is one of two edits: carry the Tier 2 clause here — on trigger-surface grounds this is arguably a better Tier 2 site than research.md — or change "offer" to match the four-square language. Leaving both as-is ships the corpus arguing with itself at the exact moment the change exists to serve.

2. /code-review in subagents.md:56 fails the invariant that 3d9f140 just added.

3d9f140 was right to drop /develop and to state the rule at subagents.md:60-62: Tier 1 names only procedures that carry the clause at their own dispatch site. Applied to the remaining list, /code-review fails it. There is no /code-review in commands/ or skills/, and the official code-review plugin is not in ~/.claude/plugins/installed_plugins.json — so it carries no clause, cannot carry one, and by the invariant grants nothing. It is the same bug the fix commit just repaired, one item over in the same sentence.

Drop it. /merge is real and carries its clause; one correct example beats two of which one is imaginary.

While there: "or asks for a PR review or a research sweep" partly escapes the invariant too, since a bare "review PR #42" reaches github.md (no clause) rather than the merge skill. The cleanest reading is that a direct request needs no standing authorization at all — the user just asked — so naming it here muddies a rule that is otherwise crisp.

Should fix before merge

  • ADR Neutral claims work that is not in the diff. ADR-175:132-133: "its agent roster omits several agents that exist. Done alongside." The roster at subagents.md:25-32 is unchanged, and it still omits Explore and general-purpose — the exact two types research.md:19 now instructs the model to fan out. Either add them to Tier 1's roster or correct the bullet; as written the ADR records a pass that did not happen.
  • Fan-out width is unbounded. All three "still ask first" conditions (subagents.md:66-67) key on being outside the invoked procedure or the stated task. None bounds width inside it. research.md:19 authorizes fanning across "several independent sources" with no ceiling, and skills/merge/SKILL.md:109 fans across dimensions. A section whose stated job is answering a cost/benefit gate (subagents.md:69-73) should carry a cost bound — one clause, e.g. a fan past ~4 agents is worth a sentence first. Without it, the answer to the cost gate is itself uncapped.
  • The clause at skills/merge/SKILL.md:100-102 sits under the "Running a single review:" label at :98, so it reads as governing the single-review corner only; the swarm dispatch at :109 falls outside it. Move the clause above :98 so it governs both corners — the four-square above is what routes to either.
  • The TaskAgent prose pass stops one file short. commands/project-init.md:306 still reads "These are subagent_type values for the Task tool", and agents/code-reviewer.md:9 lists a Task tool grant. Both are model-facing prose, same category as the line this PR corrected. The docs/hooks-and-ways.md:45,97 and docs/hooks-and-ways/teams.md:15,24,73 mentions are the PreToolUse:Task event namespace and correctly stay as they are.

Nice to have

  • subagents.md:83 says "AgentTool only" while :13 says "the Agent tool" — one file, two names for one tool. Keep AgentTool inside the quoted constant at :51 and use Agent everywhere else.
  • research.md:19 is now five sentences inside a numbered step whose siblings (:18, :20-22) run one each, and its payoff example ("six independent sources; agents return conclusions, not page dumps") is a near-copy of Tier 1's at subagents.md:77. One operative sentence carries it: asking for the research is asking for the sweep — fan Explore/general-purpose across independent sources and name the payoff. Deep-research and Workflow stay gated.
  • subagents.md went 60 → 98 lines, past the corpus's own "leave flat if <80 lines" mark (authoring.md:168), and now holds three concerns. Not a tree candidate — a child would need its own trigger surface, which the ADR correctly argues does not exist — so the answer is trimming. :64 ("This is not an override. The gate's own condition is met.") restates :52-53 and is the first line I would cut.
  • Every load-bearing external claim (the heron_brook constant, the tengu_fennel_godwit default, the unconditional return in 2.1.220) is unverifiable from this repo and unpinned to an artifact. The ADR notes behavioural unverifiability at :127-129 but not claim provenance. A version string plus where the bundle was read lets the next reader re-check instead of re-derive.

What holds up

  • The evidence-versus-override framing is sound, and with 3d9f140's invariant at subagents.md:60-62 I do not read it as an override in disguise. The text never instructs the model to disregard anything, and "the gate's own condition is met" is true when the operator typed the command that specifies the delegation. The /develop catch and the fail-closed framing in the fix commit are the right instinct applied correctly.
  • Negative consequences are real rather than token: unenforced Tier 2 drift, unverifiability from inside the repo, and Tier 1 going stale if upstream reverts. The one missing negative is the corollary of blocker 1 — partial Tier 2 coverage produces conflicting guidance in one window, which is worse than no clause at all. Worth adding now that github.md demonstrates it.
  • Declining delivery/branching and saying so in the PR body is what shows the placement rule was applied rather than assumed.
  • "Untested, not absent" for counter_steer, with the bucketing reason spelled out, is the honest read of a one-account sample.

AI-assisted review via Claude

Two blockers from the code-reviewer pass on #432.

delivery/github said to "offer" a code-reviewer on the same trigger where
delivery/merge now says to dispatch one — two ways, one query, opposite
verbs, resolved by recency. That is the contest ADR-175 declines to enter,
reintroduced by the change itself. Aligned github to dispatch, kept the
merge-strategy ask distinct from it, and recorded the corollary in the ADR:
a way covering a delegation moment has to be checked for the opposite verb.

/code-review failed the invariant added one commit earlier. It is neither a
skill nor a command in this repo, so it carries no tier-2 clause and cannot;
naming it granted authority nothing backed. Replaced with the land/ship
phrasing that does correspond to annotated sites.

Also from the review:

  - Bound the grant's width. All three "still ask first" conditions keyed on
    being outside the procedure; none capped agent count inside one, leaving
    the research fan-out and the merge swarm unbounded. The grant covers
    whether, not how wide.
  - Moved the merge clause above "Running a single review:" so it covers the
    swarm corner rather than reading as scoped to the single dispatch.
  - Added the harness built-ins Explore and general-purpose to the roster.
    The research way now names them, and the roster listed only agents/.

Corrects an overstated claim in the ADR: widening the pattern is only half
blocked. `review` is in the linter's COMMON_WORDS, but `merge` is not, and
delivery/merge already ships a lint-clean phrase form matching "let's merge
this". Reach was not the constraint — winning the match is. meta/subagents
ranked fifth on a query naming delegation outright.
@aaronsb

aaronsb commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Remediation — all findings closed at da8047d

Thanks; both blockers were real, and one of them was introduced by this PR.

Blocker 1 — delivery/github "offer" vs delivery/merge "dispatch". Correct, and it's
the sharpest finding here: the change reintroduced the recency contest its own Decision
declines to enter. github.md now dispatches, with the merge-strategy ask kept explicitly
distinct from the review question so the two don't blur. Recorded as a standing corollary in
the ADR — a way covering a delegation moment has to be checked for the opposite verb.

Blocker 2 — /code-review fails the invariant. Also correct, and the same bug as
/develop one item over in the same sentence. It's neither a skill nor a command in this
repo, so it carries no clause and can't. Replaced with land/ship phrasing that corresponds
to sites actually annotated.

Fan-out width. Bounded. The grant now covers whether, not how wide: state the number
before spawning, ask past roughly half a dozen. Without it the permission gate was answered
and the cost gate quietly wasn't.

Merge clause placement. Moved above the "Running a single review:" label so it covers the
swarm corner too.

Roster. Added Explore and general-purpose. The roster listed only agents/, while
research.md had just been changed to name built-ins that weren't in it.

ADR-155 claim — you were right and I overstated it. Verified: review is in
COMMON_WORDS (pattern.rs:49) and flagged regardless of anchoring, but merge is not, and
delivery/merge:4 already ships merge (this|it|the pr), lint-clean, matching "let's merge
this" today. Reach was never the whole problem. Both the Context and the Alternatives entry
now say what's actually true: the way has to win a match to disclose, and meta/subagents
ranked fifth at 0.3 on a query naming delegation outright. Sites don't rank.

ADR "done alongside". Was false when written — it is now true, and the sentence says
specifically what was done.

ways lint hooks/ways --check 137 files 0/0; adr lint 0 errors.

Follow-up filed separately rather than folded in: meta/subagents surfacing fifth on its own
subject is a vocabulary problem, and the ADR-155-sanctioned fix is a vocabulary: pass. That
changes matching and needs re-embedding plus scoring to verify, which doesn't belong in a
patch release.

@aaronsb
aaronsb merged commit 1c63605 into main Jul 31, 2026
3 checks passed
@aaronsb
aaronsb deleted the feat/delegation-authorization branch July 31, 2026 06:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant