fix(setup): recognise the agent-guard plugin as a complete install - #1323
Merged
Merged
Conversation
potiuk
force-pushed
the
fix/update-skill-agent-guard-plugin
branch
from
September 21, 2026 15:53
b690ba3 to
0925d3c
Compare
Check 3 of `setup-isolated-setup-update` diffs
`~/.claude/scripts/agent-guard.py` and `~/.claude/scripts/guards.d/`
against the framework, and check 4 looks for an agent-guard
`hooks.PreToolUse` entry in `settings.json`. Neither exists on the
install path the tool's own README calls "the whole installation":
the `magpie-agent-guard` plugin registers the hook from its manifest
and resolves the engine and every skill-owned guard under
`${CLAUDE_PLUGIN_ROOT}`.
A drift run against a working plugin install therefore reports the
guard as missing and sends the operator installing a second copy of
something already running. Observed on an install where the plugin
was enabled and both guards demonstrably fired.
Establish the wiring first: read `enabledPlugins`, and on a plugin
install report the plugin's version against the framework's and any
leftover user-scope copy, rather than the absence of a script that
belongs to the other wiring. Keep the script/`guards.d` diff for the
user-scope path, and give both a behavioural canary — a `git commit`
carrying a `Co-Authored-By:` trailer must be denied — so the check
rests on the guard denying rather than on files being present.
Generated-by: Claude Opus 5
potiuk
force-pushed
the
fix/update-skill-agent-guard-plugin
branch
from
September 21, 2026 16:36
0925d3c to
064d772
Compare
potiuk
added a commit
to potiuk/magpie
that referenced
this pull request
Sep 21, 2026
`main` moved by one commit while this branch was in flight — apache#1323, which edited `setup-isolated-setup-update`'s own body. The rebase carried this branch's measured row for that skill, which predates the edit; regenerating restores the true figure (5,231 → 5,578). No hand edit: `skill-token-count --write` produced it, and the pre-flight block and every `surface_hash` were confirmed in sync beforehand. Generated-by: Claude Opus 5
potiuk
added a commit
that referenced
this pull request
Sep 22, 2026
* docs(designs): reconciliation tracking for marketplace installs
A marketplace install has no memory of what it was last set up against.
The snapshot methods reconcile overrides against a new framework version
on upgrade; the marketplace method has no local lock and no such pass, so
nothing notices when a plugin moves underneath a configuration that was
written against an older build.
The design: `setup` stamps what it reconciled — a version plus a per-skill
fingerprint of the surface the project actually resolves — into the
committed lock when the project is adopted, or into gitignored local state
when it is only configured. A prek hook ships that fingerprint as a
`surface_hash:` frontmatter field, so a running skill compares its own
hash against the stamp with no file read and no hashing at runtime, which
is what makes the check work inside the sandbox. The project-wide sweep
and the latest-marketplace-version comparison live in `verify` and a new
`reconcile`, and `verify` is suggested on a fortnightly clock kept in
local state.
Proposed only; nothing is built. Records the five rejected alternatives,
including the version-only comparison that this repository's daily dev
builds would turn into a prompt nobody can action.
Generated-by: Claude Opus 5
* docs(designs): sweep when nothing is stamped, instead of staying silent
The first draft treated a missing stamp as unknown-and-silent, to avoid
greeting pre-existing adopters with a prompt they could not evaluate. That
makes the blind spot permanent: the projects certain to need reconciling
are exactly the ones adopted before the stamp existed, and they would never
be offered it.
A missing stamp now proposes a one-time full sweep. The sweep needs no
baseline, because with nothing to diff against it validates the present
instead — do the override anchors still resolve, does every
`requires_config` entry resolve through the lookup chain — both answerable
from the current tree. The guessed baseline (lock `min_version`, else the
last commit touching the committed config mapped through the marketplace
clone's history, else local mtimes) only shapes the wording, and is phrased
as the estimate it is.
Sandboxed sessions sweep the repository side and say plainly that anchor
resolution could not be checked there. The proposal is made once: accepted
it writes the stamp and the cheap per-skill check takes over, declined it
is remembered until the configuration or plugin set changes.
Generated-by: Claude Opus 5
* docs(designs): dev builds are versions, not a special case
Separate the two axes the first draft blurred. Version comparison answers
"is there something newer", and a dev build counts: nothing strips `.devN`,
rounds to the release segment, or treats a dev-to-dev move as a non-event,
and `verify` reports it as the update it is. The fingerprint answers "does
it affect this project's configuration", and gates only the prompt.
A newer dev build with no surface change is therefore an update that
`verify` reports and that no pre-flight interrupts anyone about — both
answers correct, neither suppressing the other. Choosing a dev version is
choosing frequent change; the design owes that user an honest comparison
rather than a rounded one, which is now the second reason the
release-segment-only alternative was rejected.
Generated-by: Claude Opus 5
* docs(designs): implementation plan for reconciliation tracking
Eight tasks. One of them is code — the `surface_hash` generator and its
hook, with the tests written first — and one is the validator that makes
the field mandatory. The rest are the agentic surfaces: the lock format,
the pre-flight comparison, a new `reconcile` sub-action, `verify` gaining
the sweep and the version comparison, `config` and `adopt` writing the
stamp, and the specs that state the guarantee.
Each task carries the exact files, the test or eval fixtures that fail
first, the commands to run, and its commit. Two fixes to existing text ride
in the pre-flight task because they are in the lines being rewritten: an
empty `claude plugin list --json` must read as unknown rather than as no
plugins installed, which is what a sandboxed session actually returns, and
the version comparison has to say that a dev build counts.
Generated-by: Claude Opus 5
* chore(skills): add the generated surface_hash field
Generated-by: Claude Sonnet 4.5
* feat(dev): generate a reconciliation fingerprint for every skill
Generated-by: Claude Sonnet 4.5
* feat(validator): require the generated surface_hash on every skill
Every SKILL.md now carries a surface_hash: frontmatter field, stamped by
tools/dev/skill-surface-hash.py (Task 1 of the marketplace-reconciliation
plan). validate_frontmatter now enforces it: a missing surface_hash: or
one that isn't sha256: + 16 lowercase hex characters is a HARD violation,
so a new skill can't ship without one and a corrupted value is caught.
Both error messages point at the generator, never at hand-typing a value.
Updated the inline frontmatter fixtures across test_validator.py that
build a full, violation-free SKILL.md so they keep asserting on their
own concern instead of tripping the new check.
Generated-by: Claude Sonnet 4.5
* docs(setup): specify the reconciled stamp and its marketplace flow
Documents the reconciled: block (version/at/skills, sha256 fingerprints)
that setup writes to record what a project's configuration was last
checked against, and where it lives depending on adoption state. Also
documents how marketplace adopters reach reconciliation through the
stamp instead of /magpie-setup upgrade's snapshot walk.
Generated-by: Claude Opus 5
* docs(setup): fix marketplace-exclusive framing of the reconciled stamp
The per-skill surface_hash pre-flight check and /magpie-setup reconcile
apply to every adopted or configured project regardless of install
method, per controller ruling; only the override-walk's trigger differs
(snapshot methods additionally reach it via /magpie-setup upgrade). The
previous wording in agentic-overrides.md incorrectly framed both as
marketplace-exclusive, contradicting locks.md in the same prior commit.
Also drops a locks.md callback to a rationale the file never states.
Generated-by: Claude Sonnet 4.5
* feat(setup): compare each skill against the reconciliation stamp in pre-flight
Adds a new step to the shared pre-flight block that every skill runs
first: compare the skill's own generated `surface_hash` against its
entry in the project's `reconciled:` stamp (committed lock, or the
local reconciled.json for configured-but-not-adopted projects). A
match stays silent; a mismatch distinguishes a `requires_config`
change from a moved structural anchor and proposes the matching fix;
no entry at all proposes the one-time reconciliation sweep. Declines
are remembered per skill via `acknowledged` until the hash moves
again. The check is method-agnostic — it applies the same way to a
marketplace floor and a snapshot pin.
Rides along: an unreadable `claude plugin list --json` (sandboxed
plugin cache) is now treated as unknown rather than "nothing
installed", so a sandboxed session no longer gets told to install the
project's entire floor; the version-comparison paragraph states that a
dev build compares like any other version, with the reconciliation
prompt gated on the fingerprint rather than the version delta; and a
new end-of-run item suggests `/magpie-setup verify` when it has not
run in `setup.verify_interval_days` (default 14).
Adds the `preflight-reconciliation` eval suite (5 cases) pinning the
four outcomes and the update-piggyback rule, propagates the block into
every non-setup SKILL.md via check-skill-preflight, and confirms
skill-surface-hash produces no change — the pre-flight region stays
excluded from the fingerprint by design.
Generated-by: Claude Opus 5
* fix(setup): key the reconciliation stamp by name, gate on nothing-configured, record on shown
Fix round 1 on the reconciliation pre-flight check, addressing three
Criticals and nine Importants from review.
- Stamp key is now a skill's frontmatter `name:` (e.g.
`magpie-security-issue-triage`) instead of `<plugin>/<skill>`, which
was underivable under a snapshot install (no plugin component) and
degraded silently to a false "no entry" sweep proposal.
- Step 4 now opens with a gate: no lock, no `.apache-magpie-local/`,
and no `.apache-magpie-overrides/` means nothing was ever configured
or adopted, so the step is silent and reads nothing — previously an
unadopted marketplace install got a spurious sweep proposal on every
skill invocation.
- `acknowledged` is recorded when a proposal is *shown*, never on a
decline the check does not wait for (it always continues into the
work in the same turn). Split into `acknowledged.skills` (per-skill,
keyed by name and gated on that skill's hash) and `acknowledged.sweep`
(project-scoped, gated on the installed plugin version) — a per-skill
sweep key would have meant proposing the sweep once per skill
invoked rather than once per project.
- `requires_config` vs. anchor discrimination no longer claims
causation the resolve check cannot support: an unresolved entry is
the actionable half regardless of why the hash moved; both fixes are
proposed when both apply.
- `.apache-magpie-local/reconciled.json`'s shape is now pinned (no
`reconciled:` wrapper; carries `version`/`at`/`skills` plus the
always-local keys) and its `skills` entries take precedence over the
committed lock's when both name the same skill.
- Dropped `update_available`/marketplace-clone piggybacking from the
eval suite: the block never reads the clone — that comparison is
`verify`'s alone, per the block's own step 10.
- Trimmed rationale the executing agent does not need (the three-keys
aside, "nothing above writes the stamp", the version/reconciliation
cross-axis paragraph) and made the local-file read conditional on
the lock missing an entry or a proposal being imminent, reusing that
read in step 10 — down from a per-invocation +1,168 tokens on the
smallest skill to +1,127.
Adds a sixth eval case (stamp exists but this skill is absent from
it — the more common real-world trigger than no stamp at all) and a
new preflight-floor case for the empty-`claude plugin list --json`
unknown rule, previously untested. Strips the fixtures' `Result:`
paragraphs so the model has to apply the rule rather than read the
answer.
Generated-by: Claude Sonnet 4.5
* fix(setup): skills lives in exactly one store, never both; trim steps 9-10
Fix round 2 on the reconciliation pre-flight check.
Ruling 6 supersedes Ruling 5's precedence half: the re-review found
that "local wins when both stores name a skill" was never reachable on
the common path, because a lock match short-circuits step 4 before the
local file is ever read, and would have been silently false exactly
when the lock happened to match. `skills` now lives in exactly one
store per project — the committed lock when adopted, the local file
when configured but not adopted — matching what the design said from
the start. Both `locks.md` and the block now state that invariant in
one line, plus one line for the anomaly: a `skills` entry found in
both stores is a bug or a hand edit, not a state this framework
writes; the local entry wins and `/magpie-setup reconcile` reports it
as drift. `locks.md`'s local-file JSON example now shows both shapes
— full (unadopted) and always-local-keys-only (adopted) — instead of
one ambiguous example. Ruling 5's other half (the flat JSON shape, no
`reconciled:` wrapper) is unchanged.
Also folds in the remainder of I6: steps 9 and 10's "why this lives
here" rationale is now one shared clause instead of two near-duplicate
paragraphs.
Re-ran the full `evals/setup/` suite (72 cases, print mode) rather
than just `preflight-floor/case-6-unadopted`: all cases extract and
parse cleanly, including all 8 `preflight-floor` cases; `locks.md`'s
own `lock-marketplace-parse` suite is unaffected (its extraction
boundary ends before the `reconciled:` block section this round
touched).
Generated-by: Claude Sonnet 4.5
* feat(setup): add the reconcile sub-action
Implements /magpie-setup reconcile — the one-time project-wide
sweep the shared pre-flight block proposes when a skill's own
surface-hash check finds no baseline to diff against. Walks every
configured or overridden skill, checks override-anchor resolution
and requires_config resolution, proposes fixes item by item, and
writes the reconciled: stamp (committed lock when adopted, the
flat .apache-magpie-local/reconciled.json otherwise).
Generated-by: Claude Opus 5
* fix(setup): explain why reconcile records acknowledged on decline
Ruling 7 (Task 5 review round 1): the timing difference from the
pre-flight block's per-skill check is deliberate, not a deviation
from Ruling 3 — the pre-flight never blocks for an answer, so it
has no decline event and records on show; reconcile blocks for a
real item-by-item and whole-sweep confirmation, so it has a real
decline event to hook, and an abandoned sweep should leave nothing
recorded so the proposal returns on the next run.
Generated-by: Claude Sonnet 4.5
* feat(setup): verify sweeps reconciliation and reports newer plugin versions
Generated-by: Claude Opus 5
* feat(setup): config and adopt record what they reconciled
config writes the reconciliation stamp for every skill it just
configured (Step 3b): current surface_hash, running version, and
today's date, landing in the committed lock when the project is
already adopted, else in .apache-magpie-local/reconciled.json.
adopt does the same for every skill its configuration store now
covers (Step 4d) — the one path where the stamp enters git, riding
along in the same commit the maintainer is already making. upgrade's
override-reconciliation walk (Step 5) writes the same stamp for
every override it just confirmed clean, so a snapshot project's
stamp does not go stale the moment the walk that checks it finishes.
Generated-by: Claude Opus 5
* fix(setup): config never writes the committed lock; adopt migrates the local stamp
Fix round 1 on the reconciliation-stamp work, per review ruling:
- config's reconciliation write (Step 3b) never touches the committed
lock, adopted project or not (ruling 8 / C1). Unadopted: still writes
version/at/skills to .apache-magpie-local/reconciled.json. Adopted:
writes no skills/version/at anywhere; records the per-machine fact as
acknowledged.skills["<name>"] instead, the always-local key
locks.md already reserves for it. This also closes the worktree gap:
config.md carries no main-checkout gate, so an unattended pre-flight
run could otherwise have staged into a maintainer's in-progress index
from inside a worktree. Reverted the auto-invoke justification, Hard
rule 1, and the opening summary to their pre-stamp wording, since
under this fix they are true again. Widened the stamp's skip gate
from "Step 3 wrote nothing" to the R2 nothing-configured-or-adopted
condition, so a fully-configured-but-unstamped project still gets
recorded.
- adopt's Step 4d now migrates config's local stamp into the lock on
the unadopted-to-adopted transition, instead of leaving the same
skill named in both stores (ruling 9 / C2). It also records the
version Step 2 actually read off the machine, not the (possibly
higher, ratcheted) min_version Step 2 wrote to the lock (I2).
- upgrade's override-reconciliation walk (Step 5) now also runs
reconcile's requires_config resolution check before stamping a skill
clean, so an override with intact anchors but an unresolved
requires_config entry is left out rather than reported false-clean
(I1). Documents that this reconciles overrides, not configuration,
and that reconcile is the full pass; the skip gate now matches the
surface the walk actually checks.
- locks.md: version/at describe "when this block was last written,"
not "when the project was last fully swept" (I4); the write-owner
list now includes upgrade.
- Eval fixtures: reworked step-config-stamp's two cases for the new
acknowledged.skills-vs-skills split, fixed the base-directory path
segment and hash length (M4), added the R2-skip case, and added new
step-adopt-stamp and step-upgrade-stamp suites (M5).
Generated-by: Claude Sonnet 4.5
* fix(setup): config's acknowledged.skills write is scoped to work it did
Ruling 10 fix round 2. config's Step 3b "already adopted" branch was
writing acknowledged.skills["<name>"] for every skill in scope whose
requires_config now resolves, including ones this run touched nothing
for. acknowledged.skills is a generic gate in preflight-block.md step
4 covering both a stale requires_config finding and a stale-anchor
finding without distinguishing them, so a routine no-op config run on
an adopted project was silencing anchor-drift findings it never
investigated and cannot fix.
Narrowed the trigger to what the prose already promised: config writes
acknowledged.skills only for a skill whose missing configuration this
run actually wrote (Step 3 produced the file). A skill already resolved
and untouched this run gets no entry, so its pre-flight finding stays
live. The unadopted branch (skills/version/at into
.apache-magpie-local/reconciled.json) is unchanged — it was never the
problem.
Left adopt.md 4d's hash-copy-unchanged migration alone, per the
controller's explicit instruction: recording a skill's current hash
there would claim a reconciliation adopt never performed.
Eval: added step-config-stamp/case-4-adopted-untouched-skill (the
regression this ruling closes) and widened output-spec.md's
write_stamp definition to cover both zero-write reasons (R2 gate, and
the narrowed adopted-branch trigger). 83 cases across 21 steps.
Generated-by: Claude Sonnet 4.5
* feat(dev): widen the surface-hash fingerprint to sibling detail files
skill-surface-hash.py only ever hashed a skill's own SKILL.md, so an
override anchored to a heading in a sibling detail file (setup,
pr-management-triage, security-issue-sync, ...) could drift with no
signal: the generator would not notice, and reconcile.md's
anchor-resolution check only read SKILL.md too.
surface_inputs() and surface_hash() now take the skill's directory
and fold in every sibling *.md file's anchors (sorted by filename, no
recursion into subdirectories), tagging each detail-file anchor with
the file it came from so a heading moving between files changes the
digest. A skill directory with no detail files still hashes exactly
as before. reconcile.md's anchor-resolution check now reads the same
set of files.
Generated-by: Claude Opus 5
* chore(skills): regenerate surface_hash for skills with detail files
The reconciliation fingerprint now spans a skill's sibling *.md detail
files, not only its SKILL.md. Regenerated the 18 multi-file skills
whose fingerprint widens as a result (setup, pr-management-triage,
pr-management-code-review, security-issue-sync, issue-reproducer, and
others); the 57 single-file skills are unaffected.
Generated-by: Claude Opus 5
* fix(setup): widen upgrade.md's anchor check to sibling detail files too
reconcile.md's anchor-resolution check now names SKILL.md and its
sibling detail files explicitly, but upgrade.md's Step 5 anchor check
(the override-reconciliation pass that rides along with a snapshot
refresh) stayed silent on file scope. Since upgrade writes
reconciled.skills entries off that check's outcome, the gap reproduced
the false-clean failure Task 7 closed: a skill could be stamped
reconciled while its override anchors to a detail-file heading that
moved. Matched reconcile.md's wording so the two checks describe the
same resolution surface.
Also add a test for the property the suite didn't cover directly: the
same combined anchor text split the opposite way across two detail
files must still hash differently, proving the payload tracks which
file an anchor came from and not just the anchor text.
Generated-by: Claude Sonnet 4.5
* docs(specs): state the reconciliation guarantee for marketplace installs
Add acceptance criteria to adoption-and-setup.md and
marketplace-distribution.md for the surface_hash fingerprint, the
reconciled: stamp, the reconcile sub-action, and the config/adopt/
upgrade write split the marketplace-reconciliation plan shipped.
Mark the design doc Built and record where the build departed from
it (stamp key, widened fingerprint scope, no pre-flight piggyback on
the marketplace clone, method-agnostic stamp scope, show-vs-decline
recording, config never touching the committed lock's skills map).
Note the accepted per-invocation token cost in mode-economics.md.
Generated-by: Claude Opus 5
* fix(skill-evals): grade the free-text detail field on two setup fixtures
step-reconcile/case-2-stale-anchor and step-upgrade-stamp/case-1-mixed-
checks assert on a `detail` field that the output schema defines as
free text, but the runner's default prose-field set only recognises
the plural `details`, so both were graded by verbatim string equality
and could never pass a real, correct answer that adds context beyond
the terse expected string. Extend each fixtures dir's grading-schema.json
with `detail`, the same mechanism several other suites already use for
their own custom free-text field names, so both are judged semantically
instead. Neither the output schema nor the expected values change:
case-2 still pins a re-anchor (not a config) proposal for a moved
anchor, and case-1 still pins that upgrade stamps only the override
whose requires_config resolved, excluding the one that did not.
Generated-by: Claude Sonnet 4.5
* fix(skill-evals): update preflight-floor/case-1-at-floor for the reconciliation check
case-1-at-floor's expected action of "silent" predates the shared
pre-flight block's step 4 (the reconciliation-stamp check this plan
added): the fixture has an adopted lock and overrides, all plugins
already at floor, and no reconciled: entry for the invoked skill in
either store, which is exactly step 4's "neither store names this
skill" branch — propose the one-time /magpie-setup reconcile sweep,
action=print-command. Went through every other preflight-floor case
and every other evals/setup/ step against step 4; findings recorded
in the task report rather than silently applied.
Generated-by: Claude Sonnet 4.5
* fix(setup): step 4 skips when step 3 stops, runs through an unknown one
Rulings 11-12: the reconciliation check (step 4) sat lexically before
step 5's stop-for-restart gate with no stated relationship to step 3's
outcome, leaving an agent unable to tell whether to surface a
reconciliation finding alongside a restart notice. Resolve it as one
rule: step 4 runs unless there is nothing to reconcile, or step 3 is
about to stop the run for a restart (install/update ran, commands were
only printed for no CLI, or nothing ran because of an untrusted url) —
skip it there, since the session is restarting anyway and the check
costs nothing to repeat. An *unknown* step 3 result is not a stop
(step 5 already says so) and tells the session nothing about the
project's own configuration, so step 4 now runs through it explicitly
instead of reading as a third, undefined case.
Propagate via check-skill-preflight.py --fix (65 plugin SKILL.md
copies). skill-surface-hash.py --fix confirms the hash does not move —
the generated pre-flight region stays excluded from the fingerprint.
Token counts did move (the block gained a paragraph), so
skill-token-count --write's docs/mode-economics.md update rides along
in this same commit rather than a separate one as planned: prek's
token-count hook checks the staged snapshot against every
plugins/*/SKILL.md and tools/dev/preflight-block.md file, so staging
the propagation without the regenerated counts fails that hook
deterministically (the same prek-stash interaction Tasks 1 and 9 hit
and resolved the same way). No --no-verify or SKIP used.
Also makes preflight-floor/case-8-unknown-plugin-list decidable: its
report.md never stated whether .apache-magpie-overrides/ or
.apache-magpie-local/ existed, so step 4's outcome could not be derived
from its own stated facts. Added that .apache-magpie-overrides/ exists
and no store names the invoked skill (mirroring case-1's grounding),
and updated expected.json to the reconcile-sweep proposal Ruling 12
now requires. Re-ran the five step-3-blocking cases (2/3/4/5/7); none
changed under Ruling 11, since they already expected no step 4 finding.
Generated-by: Claude Sonnet 4.5
* fix(skill-evals): fix an incidental below-floor plugin in case-6-skill-absent-from-stamp
Discovered on the required re-run after Ruling 11 (step 4 skips when
step 3 stops the run): this fixture's lock stated a 0.9.0 floor for
magpie-pr-management while its own plugin-list line reported that
plugin at 0.2.0.dev202609180100 — below the floor, and not something
anyone had reason to notice before Ruling 11 existed, since step 4
used to run regardless of step 3's outcome. With step 3 now genuinely
below-floor, step 3 stops the run for a restart and step 4 (the thing
this case exists to test — a skill absent from every stamp proposing
the sweep) never runs at all, defeating the case's own point rather
than exercising it.
The floor value was incidental, not load-bearing for anything this
case pins, so lower it to 0.1.0 (satisfied by the stated
0.2.0.dev202609180100) and say so explicitly, so step 3 passes
silently and step 4 runs as the case always meant to test.
expected.json is unchanged — the case still pins propose_sweep /
["no_stamp"].
Generated-by: Claude Sonnet 4.5
* docs(specs): document that the pre-flight reconciliation check is conditional
Rulings 11-12 shipped in tools/dev/preflight-block.md but were never
stated in the durable record: the spec's pre-flight criterion and the
design doc both still described the check as running unconditionally.
Extend adoption-and-setup.md's acceptance criterion 17 with the skip
rule (step 3 stopping the run for a restart skips it; an unreadable
plugin manager does not), and add a seventh "Where the build departed
from the design" item recording the same divergence and its reason —
a reconciliation proposal stacked onto a restart notice is the prompt
pile-up this design otherwise avoids, and the check costs nothing to
repeat next session.
Generated-by: Claude Sonnet 4.5
* fix(setup): stop the one-time reconciliation sweep proposing forever
The shared pre-flight block proposed `/magpie-setup reconcile` whenever
the running skill had no entry in the project's `reconciled:` stamp, but
`reconcile` only ever creates entries for skills the project configures.
Eleven shipped skills declare no `requires_config:` and carry no
override, so nothing an adopter can do puts them in a stamp: every
adopted project got a sweep proposal from each of them on the first
invocation after every plugin update, forever, after already sweeping.
The sweep is now proposed only when there is no `reconciled:` block in
either store at all. A stamp that exists and omits this skill is silent
— the project does not configure that skill, and the `requires_config`
step later in the same block already covers the case where it does and a
file is missing. Scope is stated broadly to match: a skill is in scope
when an override names it, or when its `requires_config:` entries
resolve from the project's own config directories.
Also in the block:
- the verify-overdue clock reads the most recent of `verified_at` and
`verify_suggested_at`, so writing `verify_suggested_at` re-arms
something; before, the nudge fired on every run;
- a skill that cannot see its own `surface_hash` skips the check
silently rather than guessing;
- every write merges into `.apache-magpie-local/reconciled.json` and
creates it, and its directory, when absent;
- `acknowledged.sweep` takes the same version fallback `version` has,
so it is defined on a snapshot install;
- restated rationale comes out of step 4.
A `skills` entry in both stores is documented as the expected
transitional state it is — `config` on one machine, `adopt` on another —
with `reconcile` offering to drop the redundant local entry, and
`unadopt` migrates the stamp back into the local file before removing
the lock rather than stranding it. `locks.md` no longer names `config`
as a writer of the committed block.
The fingerprint's anchor regex widens from `##`/`###` to include
`####`: twelve skills use fourth-level headings for real structure, and
an override anchors to those exactly as it does to a `##` step. Twelve
digests move, which is free today and expensive after the first adopter
holds a stamp. The `skill-surface-hash` hook now watches every `*.md`
directly inside a skill directory, not just `SKILL.md`, because the
fingerprint spans sibling detail files — a heading renamed in `locks.md`
moves the `setup` digest and would otherwise pass green on the PR and
fail on `main`.
Measured cost, recomputed: the block grew from 1,679 to 3,271 tokens,
+1,608 per skill including the `surface_hash:` line, +49.0% on the
smallest skill (ci-runner-audit, 3,281 to 4,889).
Generated-by: Claude Opus 4.5
* docs(setup): correct the design and spec against what shipped
- The accepted always-on cost was quoted as "+1,100 tokens / ~+34%".
Recomputed from the generated table: the pre-flight block grew from
1,679 to 3,271 tokens, +1,608 per skill with the `surface_hash:` line
counted, and +49.0% on the smallest skill (ci-runner-audit, 3,281 to
4,889) — the figure a maintainer is being asked to accept a permanent
cost against.
- Two new departed-from-design items (seven to nine): the sweep gate is
the absence of the whole stamp rather than of an entry, and a `skills`
entry in both stores is an expected transitional state with `unadopt`
migrating the map back rather than stranding it.
- `upgrade` does not record on decline — it writes stamp entries for
what its walk reconciled and nothing else. Dropped from the decline
sentence in both documents.
- The stamp's scope is stated as the broad reading it always had:
an override naming a skill, or its `requires_config:` entries
resolving from the project's own config directories. "A handful, not
the ~75" is replaced; the count follows from how much the project
configures.
- Spec criteria 17-19 and 21 restated to match, and criterion 21 gains
`unadopt`'s migration.
Generated-by: Claude Opus 4.5
* test(skill-evals): pin that an existing stamp omitting a skill is silent
The sweep proposal is gated on the absence of the whole `reconciled:`
block, not on the absence of an entry for the running skill. Two cases
pin it:
- `case-7-no-config-surface` (new) — a skill with no `requires_config:`
and no override, in a project that holds a stamp. Eleven shipped
skills are shaped this way, so under the old reading each of them
proposed the sweep on every adopted project after every plugin
update, forever.
- `case-6-skill-absent-from-stamp` — the same project state for a skill
that does have a configuration surface; its expectation moves from
`propose_sweep` to `silent`, and its fixture now states that both
`requires_config:` entries resolve, so the outcome is decidable from
the facts it gives.
`case-4-no-stamp` remains the suite's only `propose_sweep` case.
Generated-by: Claude Opus 4.5
* fix(setup): state reconcile's own trigger as the absent stamp
`reconcile.md`'s opening still described itself as what the pre-flight
names when neither store names the running skill. That is now the
silent case; the sweep is named only when no `reconciled:` block exists
in either store at all.
Generated-by: Claude Opus 4.5
* fix(skill-evals): give two preflight-floor fixtures the hash step 4 now needs
Step 4 gained a rule this wave: a skill that cannot see its own
`surface_hash` skips the reconciliation check silently rather than
guessing. `case-1-at-floor` and `case-8-unknown-plugin-list` never
stated one, so both became correctly silent and stopped matching their
`print-command` expectations — the fixtures were under-specified for the
rule, not wrong about the outcome.
Both now state the invoked skill's `name:` and `surface_hash:`, and say
explicitly that no `reconciled:` stamp exists in either store, which is
what the sweep branch keys on after this wave. Expectations unchanged.
Same "make the case decidable from its own facts" correction case-8
already took once.
Generated-by: Claude Opus 4.5
* chore(docs): re-measure setup-isolated-setup-update after the rebase
`main` moved by one commit while this branch was in flight — #1323, which
edited `setup-isolated-setup-update`'s own body. The rebase carried this
branch's measured row for that skill, which predates the edit; regenerating
restores the true figure (5,231 → 5,578).
No hand edit: `skill-token-count --write` produced it, and the pre-flight
block and every `surface_hash` were confirmed in sync beforehand.
Generated-by: Claude Opus 5
* feat(dev): generalize shared-block propagation to any number of blocks
check-skill-preflight.py solved one-source-many-copies for exactly one
shared block. check-shared-blocks.py generalizes it: the pre-flight
block keeps its exact historical auto-insert behavior (same source
path, same delimiter text, same exemption rule — none of the 65
propagated copies change), and any number of additional "declared"
blocks can now be sourced from tools/dev/blocks/<name>.md and filled
into a region a target already carries, never inserted, and only
within the skills/ tree.
Generated-by: Claude Opus 5
* chore(dev): retire check-skill-preflight.py, route the hook to check-shared-blocks
One mechanism, not two: the pre-commit hook that used to call
check-skill-preflight.py now calls check-shared-blocks.py, and the
retired script is deleted rather than kept as a second entry point.
files: widens to cover every *.md directly inside a skills/<name>/
directory and tools/dev/blocks/*.md, so a future declared block's
source and targets are both covered by the same hook.
Generated-by: Claude Opus 5
* fix(dev): make skill-surface-hash exclude declared blocks, not just preflight
skill-surface-hash.py stripped the generated region before hashing by
matching the literal preflight marker text alone. That was safe only
while zero declared blocks existed: the moment one lands in a shared
detail file, its own headings would join the carrying skill's
surface_hash, and editing shared framework text would start reading
as project-specific drift.
skill-surface-hash.py now loads check-shared-blocks.py as a module and
reuses its PREFLIGHT_RE, DECLARED_RE, and new strip_generated_regions()
directly, so the two scripts' notion of "a generated region" cannot
drift apart. The docstring states the resulting trade explicitly: an
override anchored to a heading inside a shared block will not be
detected as drift by this fingerprint, which is correct (shared text
moving is a framework change) but was previously an accident of a
regex rather than a stated contract.
Also: fix declared_seen only counting declared-block files that had an
error or a change, so a correctly-in-sync declared block was never
counted; name the renamed check-shared-blocks hook in the
skill-surface-hash pre-commit comment.
Generated-by: Claude Sonnet 4.5
* feat(setup): extract four shared-prose blocks from setup skill detail files
The five setup skill detail files (install/uninstall/upgrade/verify/
worktree-init) had duplicated procedures: the git-repo + main-checkout
pre-check (install~uninstall), ASF detection (upgrade~verify), worktree
enumeration and the sandbox-allowlist helper chain (install~upgrade).
Extract the genuinely-identical wording into tools/dev/blocks/<name>.md,
wired via check-shared-blocks.py's declared-block markers, so each is
now written once and propagated rather than hand-copied.
Also fixes a bug in check-shared-blocks.py's is_allowed_target(): it
used path.resolve(), which follows this repo's own self-adoption
symlinks (skills/<name>/ -> plugins/<family>/skills/<name>/) to their
real location outside skills/, rejecting every legitimate target here.
Switched to path.absolute(), which normalises without following
symlinks, matching how skill-surface-hash.py already treats this tree.
.lychee.toml now excludes tools/dev/blocks/: a block's relative links
are written to resolve from the host skill directory they propagate
into, not from the block source's own location.
Generated-by: Claude Sonnet 4.5
* fix(dev): re-indent declared blocks in place, restore install.md's list nesting
Fix round 1 on the shared-block extraction:
- C1 (critical): install.md's Step 12 pass 3 had lost its only
`dangerouslyDisableSandbox: true` instruction when the block replaced
the lead-in that named it. Restored, mirroring upgrade.md's and
worktree-init.md's own bolded call-site wording.
- declared_block_text() now dedents the flat block source and re-indents
it to the host's own marker indentation (DECLARED_RE gained an
`^(?P<indent>[ \t]*)` group, `re.M`). Blank lines inside a region are
never indented, so the trailing-whitespace hook's stripping and this
generator's own output do not fight to a non-convergent --fix. Restored
install.md's two regions (Step 0's main-checkout-precheck, Step 12's
worktree-enumeration + sandbox-allowlist-helper) to their original
nesting under the outer numbered list; verified via pandoc -f gfm that
both render as one continuous <ol>, matching the pre-extraction shape.
strip_generated_regions() (shared with skill-surface-hash.py) strips
indented regions the same way, with its own test.
- I2: is_allowed_target() now normalises with os.path.normpath on top of
.absolute(), closing a `skills/../docs/notes.md`-style traversal that
.absolute() alone does not catch.
- I3: added a symlinked-skill-dir test exercising is_allowed_target() and
process_declared() together, reproducing this repo's own self-adoption
layout — the exact case the .resolve()->.absolute() fix (prior commit)
was silently untested against.
- Minors: declared_seen now counts regions (8), not files (4); doctoc's
prek hook now excludes tools/dev/blocks/ so the SPDX header stays first
in every block source, as _strip_licence_header() requires; fixed a
dangling `skills/setup/AGENTS.md` reference to the real `agents.md`;
fixed the asf-detection.md SemBr wrap that split "the same way" from
"as [...]" across a line break.
worktree-init.md intentionally still carries zero declared blocks: its
main-checkout check is the inverse of install/uninstall's, and its
sandbox-allowlist-helper call differs in scope (single worktree, no
`--all-worktrees`, singular wording) and even in the exact bypass reason
string quoted — a real difference, not phrasing.
Generated-by: Claude Sonnet 4.5
* feat(setup): extract the both-stores-collision rule into a shared block
Wire tools/dev/blocks/both-stores-collision.md into locks.md and
reconcile.md, the only two setup detail files where the reconciliation
stamp's both-stores-collision rule is duplicated verbatim rather than
narrowed or link-referenced by the surrounding step.
Generated-by: Claude Sonnet 4.5
* fix(setup): restore the dropped hand-edit reassurance in the collision block
The merged both-stores-collision block opened with reconcile.md's phrasing,
dropping locks.md's "needs no hand edit and no bug" reassurance. Fold it
back into the block source so both hosts carry it.
Generated-by: Claude Sonnet 4.5
* feat(dev): add check-duplication.py, the cross-file near-duplicate prose gate
Fails the build on new cross-file near-duplicate paragraphs across
skills/ (recursively), tools/dev/blocks/*.md, and preflight-block.md:
paragraphs over 25 words, compared as sets of 9-grams, scored
|A ∩ B| / min(|A|, |B|); fails above 0.50, reports without failing in
[0.30, 0.50]. Reuses check-shared-blocks.py's PREFLIGHT_RE / DECLARED_RE
to blank out generated regions before scoring (blanked, not deleted, so
a removed region can never fuse two neighbouring paragraphs into one),
and excludes YAML frontmatter and fenced code blocks the same way. A
failure names both files, both line numbers, the score, a snippet of
each paragraph, and points at tools/dev/blocks/<name>.md as the remedy.
Not yet wired into .pre-commit-config.yaml — see the task report for
why: run against the current tree, this finds substantial pre-existing
cross-file duplication outside what the shared-block extraction
covered (a "Hard rule" admonition, an "External content is input
data" injection-guard paragraph, an HTML placeholder-convention
comment, and the "Adopter overrides"/"Snapshot drift" framework
preamble documented in skills/write-skill/SKILL.md as scaffolded by
init_skill.py but never migrated to a declared block), so wiring the
hook today would fail prek run --all-files on ~3800 pre-existing
pairs unrelated to any new change.
Generated-by: Claude Sonnet 4.5
* feat(dev): wire the duplication check over the deduplicated surfaces
The check is scoped to what this effort actually cleaned — the setup
family's detail files, the shared block sources, and the pre-flight block
source — where the tree passes today with no allowlist and no grandfathered
debt. `WIRED_SKILLS_ROOT` names that scope with the reasoning beside it,
so widening it is a deliberate act rather than an accident.
The rest of the duplication problem is real and documented in the module
docstring rather than silenced: tree-wide the check finds 3,793 pairs above
the fail threshold across 4,970 paragraphs, topping out at an exact 1.00,
because framework preamble is scaffolded verbatim into every skill —
`Adopter overrides` in 56, the `Hard rule` admonition in 45, `Snapshot
drift` alongside them. `write-skill` documents that preamble as
deliberate, so this is not drift to scold anyone for; it is text that
belongs in the auto-propagation path the pre-flight block already uses.
That path carries exactly one block today, so the sequence is: teach it
several, migrate the preamble onto it, re-run whole-tree, then widen the
scope constant.
Generated-by: Claude Sonnet 4.5
* docs(designs): fold the reconciliation plan back into its design
`docs/designs/README.md` keeps one document per subject, describing the
result rather than the phases it was built in: a plan is split out while
the work is in flight and deleted when it lands. The work has landed, and
everything the plan carried that a later reader wants — the decisions and
the rejected alternatives — is in the design, including the nine places the
build departed from it. Git keeps the task list.
Generated-by: Claude Opus 5
* refactor(setup): split the pre-flight block into a hot path and a cold sidecar
Every framework skill carries the pre-flight block always, so every token in
it is paid on every invocation of every skill. Most of it was branch
handling for outcomes that almost never occur: what to do when a plugin sits
below the floor, when a fingerprint differs, when the verify interval has
elapsed. That detail now lives in `tools/dev/preflight-detail.md`,
propagated as a whole `preflight-detail.md` file beside each skill's
`SKILL.md`, which the block points at and the agent reads only when a check
actually fails. The block is 1,754 tokens instead of 3,271, so the
reconciliation check costs each of the 65 skills +90-91 tokens over its
pre-check size rather than +1,608 — +2.8% on the smallest skill in the
catalogue instead of +49.0%.
What stayed in the block is every rule that has to bind whether or not the
sidecar was read: the prohibitions, the unknown-is-not-absent rule, and the
two things `config` may not do. The hot text decides one thing — whether to
stay silent — and says so at the top: never act on a non-silent outcome
without reading the sidecar, and if it cannot be read, say so rather than
improvising the branch. All seven graded reconciliation eval cases still
pass, including the five that now route through the sidecar.
The reason this was not done when the check shipped was wrong, and the
correction is recorded in the design's Risks. The claim was that the detail
could not move behind a pointer because the target sits in the framework
snapshot or the plugin cache, which a sandboxed session cannot read. That
conflated two policies: the Bash sandbox denies those paths, the agent's own
file-read tool does not — verified by reading one plugin-cache file with
each, one refused and one served. A second constraint was real and shapes
the result: Agent Plugins 1.0 forbids a symlink escaping the plugin root, so
a shared `skills/_shared/` include is unreachable. A sibling is not —
`plugins/<family>/skills/<skill>/` is the real directory `skills/<skill>`
symlinks into — so the sidecar lands physically inside the plugin and needs
no path to reference it. One caveat survives, on Gemini's extension install
alone, where the sidecar lands outside the workspace and a native read may
fall back to an approved shell read that prompts; that lands only on the
cold path.
Mechanically this is the propagation tool learning a second shape: a whole
generated file, not a region inside one. `skill-surface-hash.py` excludes
the sidecar by name, without which every edit to the shared detail would
move all 65 digests and tell every adopter their configuration went stale —
no digest moves here. `check-duplication.py` skips the copies and scans the
source in their place. The eval harness gains `also_include`, because a
prompt built from the block alone would grade the routing and never the
branch it routes to.
Also fixed in passing: the block source carried a doctoc TOC, which rode
into all 65 copies as a table of contents for a file none of them are. The
source joins the doctoc hook's exclude list, and the propagator strips a
leading doctoc region so a future source cannot repeat it.
Generated-by: Claude Opus 5
* refactor(setup): move the conditional pre-flight steps behind the sidecar too
The first split left four steps whose entire body fires only on a branch
that already routes to `preflight-detail.md`, so they were paying for
themselves on every invocation of every skill and earning it on almost
none: the snapshot-method remedies (step 2), the below-the-floor restart
notice (step 5), the vetted-ops proposal (step 9) and the verify
suggestion (step 10). Each keeps a stub that decides whether it is silent
and nothing more. Step 4's prose is tightened in place — it is the step
this branch introduced, and it was the largest remaining.
Step 8 deliberately stays whole. "Never run `/magpie-setup adopt`
unattended" is a prohibition whose value is that it binds whether or not
anything else was read, and it guards the one action here that would
commit a recommendation into every contributor's checkout.
The block is 1,540 tokens, down from 3,271 un-split and — the part worth
stating plainly — **139 below the 1,679 it was before the reconciliation
check existed at all**. Every one of the 65 skills carrying it is 120-126
tokens cheaper than it is on `main` while gaining the whole check;
`ci-runner-audit` goes 3,281 to 3,158. All seven graded reconciliation
cases still pass, and no `surface_hash` digest moves.
Generated-by: Claude Opus 5
* refactor(setup): reduce steps 8, 9 and 10 to their triggers
Three steps still carried their reasoning inline although their bodies
only fire on a branch. Step 9's framing ("not a pre-flight check") became
a shared lead-in for the pair, its `never apply` prohibition moved next to
the proposal it constrains, and step 10's config-resolution chain moved to
where the value is explained. Step 8 keeps the prohibition itself —
`adopt` commits a recommendation into every contributor's checkout, the
block is the only place that rule is written, and it has to bind whether
or not the sidecar was read — while the mention it asks for, which is
conditional on step 7 having written something, moves out.
The block is 1,448 tokens: 231 below the 1,679 it was before the
reconciliation check existed, so each of the 65 skills carrying it is
212-218 tokens cheaper than on `main` while gaining the whole check.
`ci-runner-audit` goes 3,281 to 3,066, -6.6%.
This is where the split stops, and the reason is now written into the
design. What remains inline is each step's own test for whether it is
silent, and a step cannot know it has nothing to say without evaluating
that test — so moving the triggers behind the pointer would mean reading
the 2,335-token sidecar on every run to save 1,448. The same arithmetic
rules out replacing the block with a bare pointer.
Graded reconciliation evals 7/7; no `surface_hash` digest moves.
Generated-by: Claude Opus 5
* feat(setup): answer the pre-flight with a tool instead of re-deriving it
Most of what the shared pre-flight block asked a model to do was not
judgement. Read a lock; order two versions as PEP 440; compare two
hashes; subtract two dates; decide whether a proposal was already shown.
Every skill paid for that to be re-derived from prose on every
invocation, and nothing tested it.
`tools/setup-preflight` does it instead — stdlib-only, 50 tests — and the
block runs it as one command. `{"verdict": "ok"}` is silent and is the
ordinary answer; anything else lists findings, each naming the
`preflight-detail.md` section whose rules apply. The block is 561
tokens, against 1,679 before this reconciliation work began and 3,271 at
its peak, so each of the 65 skills carrying it is 1,099-1,105 tokens
cheaper than on `main` while gaining the whole check. `ci-runner-audit`
goes 3,281 to 2,179, -33.6%.
It also gives the project/skill split somewhere to live. **Project**
scope — the lock, snapshot drift, the marketplace floor — is identical
for every skill in a tree, so it is memoised against the inputs it
depends on and later skills in a session pay only for their own
fingerprint comparison. **Skill** scope is the per-skill half. Prose
could not make that distinction pay, because the agent re-read the whole
block either way.
Two rules are the reason this is code rather than text. *Unknown is
never absent*: a plugin listing that could not be read is `None`, not
`{}`, and an empty parsed listing is treated as unknown too, because
inside a sandbox the read-denied cache and a plugin-free harness are
indistinguishable. *A dev build is a version like any other*: nothing
strips `.devN`, so `0.2.0.dev…` is below `0.2.0` while `0.10.0` is above
`0.9.0` — the two orderings a string comparison gets wrong in opposite
directions. Both are now tests rather than paragraphs.
The checker is copied into the gitignored `.apache-magpie-local/` by
`config` and refreshed by `upgrade`, not run from the plugin: Bash can
neither read nor execute the plugin cache under the sandbox the framework
itself recommends, which is exactly the sandboxed marketplace install
this whole feature targets. That means an unattended `config` run can
place an executable in the checkout — framework code of the same
provenance as the installed plugin, gitignored, gone with the directory,
and said out loud rather than done quietly.
The eval follows the seam: its cases now supply the checker's verdict and
grade the proposal the rules produce, because the classification they used
to grade is covered by pytest. 7/7 graded, and no `surface_hash` digest
moves.
Generated-by: Claude Opus 5
* refactor(setup): ship the pre-flight rules in the tool and emit them per finding
The sidecar was an intermediate step. It put the rules where a run could
reach them cheaply, but it did so by generating 2,516 tokens into 65 skill
directories so that a run needing one 150-token section could find it —
the same text sixty-five times, to say one thing.
The sections now ship inside `tools/setup-preflight` as `sections/*.md`,
and the verdict carries the text for the findings it actually reported.
One call returns both what is true and what to do about it: no second
file, no per-skill copy, and the ordinary `{"verdict": "ok"}` carries no
rules at all. A run that needs one section pays for one, where before it
read all 2,516 tokens to use a fraction.
This removes machinery rather than adding it. The sidecar propagation
path in `check-shared-blocks.py` is gone, along with its exclusion from
`skill-surface-hash.py` and from `check-duplication.py`; both exclusions
existed only because 65 identical generated files would otherwise have
moved every digest and flooded the duplication gate. The `step-0`
circularity goes too — a missing checker can no longer be told to consult
a file the same install step would have delivered, so the block carries
those three sentences itself.
The block is 585 tokens, against 1,679 before any of this reconciliation
work and 3,271 at its peak, so each of the 65 skills carrying it is
1,075-1,081 tokens cheaper than on `main` while gaining the whole check.
`ci-runner-audit` goes 3,281 to 2,203, -32.9%.
Emitted text is stripped of the SPDX header and of any doctoc block: both
are correct in the file and pure noise in someone's context, where every
emitted section is paid for. The sections directory is excluded from the
doctoc hook for the same reason — a table of contents for a one-section
file says nothing.
Keeping the prose beside the logic also keeps them honest: a finding that
named a section which did not ship would reach the agent as a rule-less
instruction to act, so `sections.load` raises instead, and a test asserts
every section `core.py` can name actually exists.
The eval now grades what the runtime produces — the block as the prompt,
a fixture carrying the verdict and its rules exactly as the command emits
them. 7/7 graded; 56 tests in the tool, 204 in `tools/dev`; no
`surface_hash` digest moves.
Generated-by: Claude Opus 5
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.
Summary
setup-isolated-setup-updateonly knows the user-scope agent-guard wiring: check 3 diffs~/.claude/scripts/agent-guard.pyand~/.claude/scripts/guards.d/, check 4 looks for an agent-guardhooks.PreToolUseentry insettings.json. Neither exists on the install pathtools/agent-guard/README.mdcalls "the whole installation" — themagpie-agent-guardplugin registers the hook from its manifest and resolves the engine plus every skill-owned guard under${CLAUDE_PLUGIN_ROOT}.magpie-agent-guard@apache-magpieenabled, no script copy, and both guards demonstrably firing.Type of change
.claude/skills/<name>/) — see test plan for why no eval fixturetools/<system>/*.md)tools/*/withpyproject.toml)docs/,README.md,CONTRIBUTING.md)projects/_template/)prek, workflows, validators)How it works
Check 3 reads
enabledPluginsfirst. Withmagpie-agent-guard@apache-magpiepresent, the absent script copy is stated to be the expected shape rather than drift, and what gets surfaced instead is the plugin's version against the framework's (refresh is/plugin) plus any leftover user-scope copy from an earlier hand-wiring. The existing script +guards.ddiff is kept verbatim for the non-plugin path. Check 4's agent-guard clause gains the same condition, so asettings.jsonwith no agent-guard hook reads as correct under the plugin.Both paths then share one canary: a
git commitwhose message carries aCo-Authored-By:trailer must be denied by the bundledcommit-trailerguard. A command that goes through means the hook is not firing, whatever the files say — which is the only claim the check is really trying to make.Test plan
prek run --all-filespasses (32 hooks, exit 0).git commitwith aCo-Authored-By:trailer → denied bycommit-trailer; a second commit → denied by the operator's own trailer guard. Under the old text this install reads "agent-guard not installed"; under the new text it reads as correctly wired.RFC-AI-0004 compliance
/pluginare named inside the skill's existing Claude Code branch, after the Codex / Gemini routing has already returned. See the note below about the generated score.Linked issues
None.
Notes for reviewers (optional)
docs/vendor-neutrality.mdshifts one skill from capability-pure (14 → 13) to portable (61 → 62), because the Claude branch now names a Claude plugin id.vendor-coupledstays 0. If you would rather the skill stay capability-pure, the alternative is a generic "the harness may supply the guard from an installed extension" phrasing — less useful, since the operator has to know the exact id to check for.docs/mode-economics.mdmoves because theskill-token-counthook remeasures the skill.allowedDomainsstaleness and the dispatcher hook flavour. Each is its own PR and they do not overlap in the file.🤖 Generated with Claude Code