ci(agents): gate that every .claude/agents/ definition declares a model (#6803) - #6836
Merged
Merged
Conversation
…el (#6803) An agent definition with no `model:` in its frontmatter INHERITS the dispatching session's model, so the role's tier becomes a property of whoever dispatched it and when, rather than a property of the role. `os-dev.md` already carries `model: opus` (#6686 / PR #6688) — this adds the mechanism that keeps it there and covers the next definition someone adds. Why the assertion belongs on the definition rather than at the call site: the caller-side half already existed and did not hold. `.claude/skills/pm-dispatch/ SKILL.md` §5 has said "pass `model: \"opus\"` on every dev dispatch" since before the incident, and a seat that had read it still dispatched twelve os-dev agents in a row without passing it, with no signal. The frontmatter holds regardless of caller compliance. The gate asserts PRESENCE, never which tier. Which model a role runs on is maintainer policy (#6803 puts changing it out of scope), and a policy change should not have to edit a gate to land. `model: inherit` stays legal for a role that genuinely follows its caller, but only with a written justification the script re-checks against the live file — so a deliberate inherit is a recorded decision and a silent one is red. Every absence is red, never a skip (#4690): missing directory, no .md files, no frontmatter, empty value, unresolvable value, unjustified inherit, and stale or dead INHERIT_JUSTIFIED entries. 18 self-test cases pin those red paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F8q5J1MQyocgtNspb15fSn
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
os-project-manager
marked this pull request as ready for review
August 9, 2026 01:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6803
What this PR does, and what it deliberately does not
The card listed three fixes. Two of them were already in the tree before this PR started, and the audit found nothing to fix. So the only thing this PR builds is the item the card flagged as "worth considering": the gate.
mainate120a5a1dmodelinos-dev.mdmodel: opusat.claude/agents/os-dev.md:9, by PR #6688 for #6686, merged2c57837ecat 13:09:56Z.claude/agents/holds exactly one file.claude/skills/pm-dispatch/SKILL.md:1370-1373.claude/agents/at allThe premise had already expired when the card was filed
This is worth stating plainly because it changes how the card should be read. #6803 was filed at 18:16Z. Its item 1 landed at 13:09Z — five hours earlier — via PR #6688, closing #6686, which was filed at 12:46Z by the spec-tooling seat after the actual incident. The services-lane cross-link on this card correctly caught that item 3 was already done, and correctly identified #6685 as a duplicate, but neither seat noticed that #6686 had already landed item 1. Three issues were filed on one subject inside six hours; the first one fixed it.
So
os-dev.mdneeded no edit, and this PR makes none to it.Item 2 — the audit, in full
.claude/agents/contains exactly one file:os-dev.md, which declaresmodel: opus. There are no siblings to fix, and none that intentionally inherit — so there is no "documented inherit" to write. I checked the adjacent surfaces too:.claude/workflows/holds one JS file (not an agent), and neither.claude/settings.jsonnor.claude/launch.jsoncarries model configuration.A one-file audit is a weak deliverable on its own — it is true today and says nothing about tomorrow. That is precisely why the gate is the real answer to item 2: it converts a point-in-time observation into a standing assertion, so the next definition added by copying this one and trimming the frontmatter is caught at authoring time.
The gate
scripts/check-agent-model-declared.mjs, wired aspnpm check:agent-model-declaredand run in the TypeScript Type Check job besidecheck:skill-frame-syncandcheck:skill-compatibility— the family the card pointed at. Neither lint job carries a paths filter, so it cannot go dormant on exactly the PR that breaks it.Why the assertion belongs on the definition, not at the call site. The caller-side half already existed and did not hold.
SKILL.md§5 has carried themodel: "opus"clause since before the incident, and per the services-lane comment on this card, a seat that had read it still dispatched twelve os-dev agents in a row without passingmodel, with no signal that anything was wrong. Prose in the caller is a thing to remember; the frontmatter holds regardless of caller compliance.It asserts presence, never which tier. This is the one design decision worth challenging, so I will state the reasoning rather than bury it. Encoding "os-dev must be opus" in the gate would mean a legitimate maintainer policy change has to edit a check script to land. The defect #6803 exists for is the empty slot, which is mechanical and objective; the tier is a decision, and #6803 puts changing it explicitly out of scope. The value is documented where it belongs — in the file's own frontmatter comment, which already explains the pin.
model: inheritstays legal, but only in writing. A role that genuinely should follow its caller can say so, but an unexplained inherit is indistinguishable in effect from the bug. Soinheritis accepted only for files listed inINHERIT_JUSTIFIED, each with a stated reason and arationaleregex that must still match the live file — edit the justification away and the entry dies with it. The list is empty today; it exists so the first role that wants to inherit has a way to record it. This is the card's own distinction ("a silent inherit is the defect; a documented one is a decision") made mechanical, reusing the self-invalidating-exemption shape fromcheck:skill-compatibility.Every absence is red, never a skip (#4690). Missing directory, no
.mdfiles, no frontmatter, unterminated frontmatter,model:present but empty, a value that is neither a tier alias nor aclaude-model id, an unjustifiedinherit, a justification whose reason is gone, one naming an unscanned file, and one on a file that now pins a tier.Verification
pnpm check:agent-model-declared— 18 self-test cases, then the real tree:Reverse verification — predicted RED, and red is what happened. The gate asserts exactly the presence of the line, so deleting
model: opusfromos-dev.mdreproduces its own pre-#6688 state. It exits 1 and names the defect:os-dev.mdwas then restored byte-identical — it does not appear in this PR's diff.Also green, to confirm no regression in the family:
check:skill-frame-sync(12 cases, 4 copies isomorphic),check:skill-compatibility(18 cases, 11 SKILL.md files),check:doc-authoring(373 files),check:nul-bytes(6367 tracked files), andeslinton the new script.lint.ymlre-parsed as YAML after editing.No changeset: this PR touches only
.claude/-adjacent tooling,scripts/, CI wiring and a rootpackage.jsonscript. It releases nothing, so it carriesskip-changeset.The trade-off the card asked me to check
Whether any caller legitimately wants an
os-devon a different tier. The trade-off is real but bounded, and it is already paid. A frontmatter pin does override inheritance for every caller — but not the explicit argument: the Agent tool resolves explicitmodelon the dispatch → frontmatter → inherit, so a caller that genuinely needs a different tier passesmodeland still wins.os-dev.md's own comment says exactly this ("that override takes precedence over this line, so pinning here costs nothing"). I found no dispatch site in the repo requesting a non-opusos-dev. Note also that this PR does not create that trade-off — PR #6688 did, five hours before the card was filed; this PR only stops the pin from being removable in silence.One judgement the card did not settle
The card told me to write explanatory prose in Chinese "per repo convention". The repo's binding rule now says the opposite: AGENTS.md §Communication carries the maintainer ruling of 2026-08-08 (#6692), quoted verbatim and untranslated —
— and
os-dev.md:182-186restates it as "title and explanatory prose in English". So this body is English. The dispatch prompt carried the pre-#6742 rule, which is itself an instance of the failure class #6803 cites in its own "Related" line (#6801: a rule that lives in the wrong file reaches the wrong readers).Generated by Claude Code