docs: finish reconciling the agent write fence across the toolchain
The previous round taught agent-audit's validator to permit disallowedTools but left the skill that writes agents still forbidding it, in six places. Running agent-author on any of the three fenced orchestrators would have stripped the fence, and nothing would have caught it: the validator's allowlist is a permit list, so an absent field passes. The template was the worst of them, since its comment is copied verbatim into every new plugin-scope agent. Where a list had to be restated it is now a pointer to field-inventory.md's apm-agent-allowlist instead -- the same data validate.sh reads -- because a roster copied into a template goes stale one step further out than the roster itself. Where the text has to teach something it teaches the shape rule rather than the exception: tools is an allowlist whose vocabulary differs per harness, so verbatim copy makes one value wrong on one target; disallowedTools is a denylist, where an unrecognised name denies nothing, so the worst case is a missing fence rather than a wrongly granted capability. ADR-0016's amendment claimed an unrecognised key is inert on Copilot while the same ADR's Context says that behaviour is unconfirmed by research -- asserting as settled the exact thing it flags as unknown, and justifying it with apm's compile-time behaviour, which says nothing about Copilot's runtime. It is rewritten into labelled tiers: confirmed for Claude Code with citations, inferred by analogy for Copilot with the analogy's limits stated, unverified where it is unverified, and the residual risk accepted explicitly with its blast radius. It also no longer claims to restore a write sandbox: the denylist does not deny Bash, which these agents inherit and legitimately need. docs/hooks.md called the old root hooks.json a stale sync artifact -- it was added in the plugin's creating commit and pointed at by main's Copilot manifest -- and claimed both ecosystems now resolve hooks/hooks.json. Copilot does not: its hooks field has no default and no compiled manifest declares one, so it resolves nothing. Recorded as the gap it is, with re-injection noted as a follow-up rather than asserted away. Its event list is marked partial. Also: new-agent.bats asserted a hardcoded four-field allowlist and would have rejected a scaffolded agent carrying the field the ADR now blesses; it reads field-inventory.md too. And ADR-0016's premise that Claude's tools: is space-separated was wrong -- it takes a comma-separated string or a YAML list. The incompatibility with Copilot is the vocabulary, not the punctuation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
This commit is contained in:
@@ -41,6 +41,8 @@ Fall back to raw shell only when no skill covers it.
|
|||||||
- Install the `vale` binary — required by the `vale-audit-prefilter-skill`/`-agent` pre-commit hooks. Their `files:` patterns are `.apm/`-scoped: `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` and `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$`. Only the authoring source triggers them — a `SKILL.md` in the generated mirror matches neither pattern, so prose findings surface only when you edit the file you are supposed to be editing. Without the binary the hooks fail with a bare "command not found" and no install pointer. `brew install vale` (macOS), `snap install vale` (Linux), `choco install vale` (Windows), or see https://vale.sh/docs/vale-cli/installation/. No `vale sync` needed — the `Kyberforge` styles are committed under `plugins/kyberforge/.apm/skills/{skill-audit,agent-audit}/assets/vale/styles/`, not downloaded packages (see ADR-0014).
|
- Install the `vale` binary — required by the `vale-audit-prefilter-skill`/`-agent` pre-commit hooks. Their `files:` patterns are `.apm/`-scoped: `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` and `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$`. Only the authoring source triggers them — a `SKILL.md` in the generated mirror matches neither pattern, so prose findings surface only when you edit the file you are supposed to be editing. Without the binary the hooks fail with a bare "command not found" and no install pointer. `brew install vale` (macOS), `snap install vale` (Linux), `choco install vale` (Windows), or see https://vale.sh/docs/vale-cli/installation/. No `vale sync` needed — the `Kyberforge` styles are committed under `plugins/kyberforge/.apm/skills/{skill-audit,agent-audit}/assets/vale/styles/`, not downloaded packages (see ADR-0014).
|
||||||
- `vale` is also a **pre-push** dependency, not only pre-commit. `check-vale-style-sync` runs six glob-coverage probes by invoking `vale --config` — they are the only assertions in it that catch a `.vale.ini` glob typo, the failure mode where every text-level check stays clean while vale lints zero files. Missing `vale` is therefore a hard failure there. The opt-out is `CHECK_VALE_STYLE_SYNC_ALLOW_MISSING_VALE=1`, and it is **not** `SKIP=`: the hook still runs and still asserts everything verifiable from file text, but the six probes do not, and its summary says so explicitly — `Vale style sync check passed (text-level only, vale unavailable): … 0 glob probe(s) verified`. Use it only on a machine that genuinely cannot install `vale`, and read that summary line as "the glob axis was not checked", not as a pass.
|
- `vale` is also a **pre-push** dependency, not only pre-commit. `check-vale-style-sync` runs six glob-coverage probes by invoking `vale --config` — they are the only assertions in it that catch a `.vale.ini` glob typo, the failure mode where every text-level check stays clean while vale lints zero files. Missing `vale` is therefore a hard failure there. The opt-out is `CHECK_VALE_STYLE_SYNC_ALLOW_MISSING_VALE=1`, and it is **not** `SKIP=`: the hook still runs and still asserts everything verifiable from file text, but the six probes do not, and its summary says so explicitly — `Vale style sync check passed (text-level only, vale unavailable): … 0 glob probe(s) verified`. Use it only on a machine that genuinely cannot install `vale`, and read that summary line as "the glob axis was not checked", not as a pass.
|
||||||
- Run `bash tests/run-tests.sh` before considering any change done — it runs every `test-*.sh` script in the repo plus the bats suite (`--bats-only` for just bats). First run auto-initializes the bats submodules; no manual `git submodule update` needed.
|
- Run `bash tests/run-tests.sh` before considering any change done — it runs every `test-*.sh` script in the repo plus the bats suite (`--bats-only` for just bats). First run auto-initializes the bats submodules; no manual `git submodule update` needed.
|
||||||
|
- A suite that exits 77 because a dependency is missing is reported as SKIPPED, and does **not** fail an ad-hoc run. The pre-push hook invokes the same script as `--strict` (`RUN_TESTS_STRICT=1` is equivalent), where a skip **does** fail the push: at pre-push a skip means one of the dependencies above is absent on this machine, so the gate would otherwise report success having run fewer suites than it appears to. Without vale, for instance, three suites skip (`test-check-vale-style-sync.sh`, `test-vale-hooks-consumer.sh`, `test-vale-wrap.sh`) and the strict failure names each one and what to install.
|
||||||
|
- `tests/run-bats.sh` derives the set of `.bats` files it expects from `git ls-files`, so a `.bats` file deleted from the worktree but still tracked in the index fails the run rather than silently shrinking the suite. Remove one with `git rm` (or stage the deletion) when the removal is intentional; an untracked new `.bats` file is picked up and needs no ceremony.
|
||||||
- Pushing runs 12 repo-defined pre-push hooks, not just the test suite — `run-tests` and `check-manifests`, plus generated-content drift gates (`check-plugin-content-sync`, `check-marketplace-mirror-sync`, `check-vale-style-sync`, `check-scope-walkup-sync`), apm's own gates (`apm-marketplace-check`, `apm-audit-ci`, `apm-pack-check-clean`), host validators (`validate-plugins`, `validate-marketplace`, both needing the `claude` CLI), and `check-release-needed`. Run `pre-commit run --hook-stage pre-push --all-files` locally — one command, the whole gate. That command reports **14**, not 12: pre-commit's own `meta` hooks, `check-hooks-apply` and `check-useless-excludes`, declare no `stages:` and so run at every stage including this one.
|
- Pushing runs 12 repo-defined pre-push hooks, not just the test suite — `run-tests` and `check-manifests`, plus generated-content drift gates (`check-plugin-content-sync`, `check-marketplace-mirror-sync`, `check-vale-style-sync`, `check-scope-walkup-sync`), apm's own gates (`apm-marketplace-check`, `apm-audit-ci`, `apm-pack-check-clean`), host validators (`validate-plugins`, `validate-marketplace`, both needing the `claude` CLI), and `check-release-needed`. Run `pre-commit run --hook-stage pre-push --all-files` locally — one command, the whole gate. That command reports **14**, not 12: pre-commit's own `meta` hooks, `check-hooks-apply` and `check-useless-excludes`, declare no `stages:` and so run at every stage including this one.
|
||||||
- **Two** pre-push hooks need the network, for one shared reason: root `apm.yml`'s `marketplace.packages[]` contains exactly one remote entry (`mattpocock-skills`, `source: mattpocock/skills`), and resolving it needs a `git ls-remote`. `apm-marketplace-check` resolves every entry and is `always_run`, so it fails with `No cached refs (offline)`. `apm-pack-check-clean` (`apm pack --check-versions --check-clean --dry-run`) re-resolves the same entry and fails with `Error: Git network timeout during ls-remote`. Pinning the entry to an exact version does **not** remove the call — an exact pin still ls-remotes. `--offline` rescues neither. To push without a network, skip both using pre-commit's own mechanism: `SKIP=apm-marketplace-check,apm-pack-check-clean git push`. Skip those two alone — verified under `unshare -rn`, the other ten pre-push hooks pass offline because they are real local checks, and adding one of them to `SKIP` disarms it silently.
|
- **Two** pre-push hooks need the network, for one shared reason: root `apm.yml`'s `marketplace.packages[]` contains exactly one remote entry (`mattpocock-skills`, `source: mattpocock/skills`), and resolving it needs a `git ls-remote`. `apm-marketplace-check` resolves every entry and is `always_run`, so it fails with `No cached refs (offline)`. `apm-pack-check-clean` (`apm pack --check-versions --check-clean --dry-run`) re-resolves the same entry and fails with `Error: Git network timeout during ls-remote`. Pinning the entry to an exact version does **not** remove the call — an exact pin still ls-remotes. `--offline` rescues neither. To push without a network, skip both using pre-commit's own mechanism: `SKIP=apm-marketplace-check,apm-pack-check-clean git push`. Skip those two alone — verified under `unshare -rn`, the other ten pre-push hooks pass offline because they are real local checks, and adding one of them to `SKIP` disarms it silently.
|
||||||
- Author commits with `git:git-commits` — it validates Conventional Commits (enforced at `commit-msg`) for you.
|
- Author commits with `git:git-commits` — it validates Conventional Commits (enforced at `commit-msg`) for you.
|
||||||
|
|||||||
@@ -118,8 +118,10 @@ correction) sorted what they document into three buckets:
|
|||||||
match.
|
match.
|
||||||
- ADR-0016 (a narrower decision discovered while designing issue #89) turned out to gate how
|
- ADR-0016 (a narrower decision discovered while designing issue #89) turned out to gate how
|
||||||
issue #90 had to re-author plugin-scope agents: `.apm/agents/*.agent.md` compiles verbatim to
|
issue #90 had to re-author plugin-scope agents: `.apm/agents/*.agent.md` compiles verbatim to
|
||||||
both Claude and Copilot, so those files carry only `name`/`description`/`model`/`source_keys` —
|
both Claude and Copilot, so those files carry only the fields in the `apm-agent-allowlist` section
|
||||||
existing dual-file `<name>.md`+`<name>.agent.md` pairs could not be raw-moved, only re-authored.
|
of `plugins/kyberforge/.apm/skills/agent-audit/references/field-inventory.md` (as amended
|
||||||
|
2026-08-14: `name`/`description`/`model`/`source_keys`/`disallowedTools`) — existing dual-file
|
||||||
|
`<name>.md`+`<name>.agent.md` pairs could not be raw-moved, only re-authored.
|
||||||
- Two follow-up issues tracked the remaining work: #89 (`skill-author`/`agent-author` routing
|
- Two follow-up issues tracked the remaining work: #89 (`skill-author`/`agent-author` routing
|
||||||
adaptation — closed, merged in #93) and #90 (the actual repo conversion, which also deleted
|
adaptation — closed, merged in #93) and #90 (the actual repo conversion, which also deleted
|
||||||
`plugin-author`/`marketplace-author` — tracked through to merge; treat #90's own state as the
|
`plugin-author`/`marketplace-author` — tracked through to merge; treat #90's own state as the
|
||||||
|
|||||||
@@ -22,13 +22,17 @@ Code and Copilot CLI targets. This is unlike:
|
|||||||
Because the agent primitive ships the same frontmatter unchanged to both harnesses, two
|
Because the agent primitive ships the same frontmatter unchanged to both harnesses, two
|
||||||
concrete incompatibilities surface:
|
concrete incompatibilities surface:
|
||||||
|
|
||||||
1. **`tools:`** — Claude Code expects a space-separated tool-name string; Copilot CLI expects a
|
1. **`tools:`** — Claude Code expects tool names drawn from its own vocabulary, as a
|
||||||
list drawn from its own alias vocabulary (`execute`/`read`/`edit`/`search`/`agent`/`web`). A
|
comma-separated string or a YAML list (`agent-definition.md:37`); Copilot CLI expects a list
|
||||||
value correct for one harness is wrong for the other.
|
drawn from a different alias vocabulary (`execute`/`read`/`edit`/`search`/`agent`/`web`). The
|
||||||
|
incompatibility is the vocabulary, not the punctuation: a value correct for one harness names
|
||||||
|
tools the other does not have.
|
||||||
2. **Claude-only knobs with no Copilot equivalent** — `isolation`, `maxTurns`, `effort`,
|
2. **Claude-only knobs with no Copilot equivalent** — `isolation`, `maxTurns`, `effort`,
|
||||||
`memory`, `permissionMode`. Writing any of these means Copilot's copy carries frontmatter
|
`memory`, `permissionMode`. Writing any of these means Copilot's copy carries frontmatter
|
||||||
keys it doesn't recognize at all. Whether Copilot's agent loader ignores unknown keys or
|
keys it doesn't recognize at all. Whether Copilot's agent loader ignores unknown keys or
|
||||||
errors on them is unconfirmed by research.
|
errors on them is unconfirmed by research. *(Still unconfirmed as of the 2026-08-14 amendment
|
||||||
|
below, which admits `disallowedTools` as an explicitly accepted risk rather than by resolving
|
||||||
|
this question.)*
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
@@ -81,25 +85,61 @@ delimited string of its own tool names, Copilot CLI reads it as a list drawn fro
|
|||||||
alias vocabulary (`execute`/`read`/`edit`/`search`/`agent`/`web`), so one value is wrong on one
|
alias vocabulary (`execute`/`read`/`edit`/`search`/`agent`/`web`), so one value is wrong on one
|
||||||
harness. That reasoning stands, and `tools:` stays out of every plugin-scope agent.
|
harness. That reasoning stands, and `tools:` stays out of every plugin-scope agent.
|
||||||
|
|
||||||
A **denylist** has no such conflict.
|
A **denylist** has no such conflict. The evidence for that splits three ways, and this amendment
|
||||||
|
states which part is which rather than asserting the whole as settled.
|
||||||
|
|
||||||
|
**Confirmed — Claude Code honours it for plugin subagents.**
|
||||||
`plugins/kyberforge/docs/research/docs/claude-code-plugins/agent-definition.md:39` documents
|
`plugins/kyberforge/docs/research/docs/claude-code-plugins/agent-definition.md:39` documents
|
||||||
`disallowedTools` as a "Denylist applied before `tools`… Takes precedence over `tools`", and — the
|
`disallowedTools` as a "Denylist applied before `tools`… Takes precedence over `tools`", and — the
|
||||||
part that matters here — it is **not** in that document's plugin-subagent ignore list. Line 99
|
part that matters here — it is **not** in that document's plugin-subagent ignore list. Line 99
|
||||||
names exactly three fields plugin agents silently ignore: `hooks`, `mcpServers`, `permissionMode`.
|
names exactly three fields plugin agents silently ignore: `hooks`, `mcpServers`, `permissionMode`.
|
||||||
`disallowedTools` is absent from that list, so it is honoured for plugin agents. On the Copilot
|
`disallowedTools` is absent from that list. Claude Code is also the harness where the fence is
|
||||||
side, the same verbatim-copy behaviour that makes `tools:` dangerous makes `disallowedTools`
|
actually wanted, so the field earns its place on this evidence alone.
|
||||||
harmless: an unrecognised frontmatter key is inert, and unlike a wrong `tools:` value it cannot
|
|
||||||
grant or misroute anything — the worst case is that the fence is absent on that harness, which is
|
**Inferred — the field is very likely inert on Copilot CLI, but by analogy, not by documentation.**
|
||||||
the status quo the decision above already accepted.
|
`plugins/kyberforge/docs/research/docs/github-copilot-plugins/troubleshooting.md:50` and `:53`
|
||||||
|
record Copilot *silently ignoring* two agent frontmatter fields it does not process (`mcp-servers`
|
||||||
|
and `metadata` outside the cloud runtime) rather than erroring on them. That is a documented
|
||||||
|
tolerance for *known-but-unprocessed* keys, which is adjacent to, not identical to, tolerance for
|
||||||
|
an *unknown* key. No stronger evidence exists: a sweep of the vendored Copilot corpus
|
||||||
|
(`agent-definition.md`, `api-reference.md`, `troubleshooting.md`, `configuration.md`) documents
|
||||||
|
unknown-key handling nowhere.
|
||||||
|
|
||||||
|
**Unverified — Copilot's loader behaviour on an unrecognised key.** Context item 2 above says this
|
||||||
|
is unconfirmed by research and that remains true; nothing found since changes it. An earlier
|
||||||
|
revision of this amendment claimed "an unrecognised frontmatter key is inert" as settled fact and
|
||||||
|
attributed it to apm's verbatim-copy behaviour. That attribution was a non-sequitur — verbatim copy
|
||||||
|
describes what *apm* does at compile time and says nothing about what *Copilot* does at load time —
|
||||||
|
and the claim contradicted this ADR's own Context section.
|
||||||
|
|
||||||
|
**So this is an accepted risk, stated as one.** Blast radius if the inference is wrong and Copilot
|
||||||
|
errors on the key: the three affected plugin-scope agents fail to load under Copilot CLI. It is
|
||||||
|
loud, not silent; it is confined to three agents in three plugins; no other primitive and no Claude
|
||||||
|
Code path is affected; and the remedy is a one-line frontmatter deletion. What the denylist shape
|
||||||
|
*does* rule out categorically — independent of loader behaviour — is the failure mode that motivated
|
||||||
|
dropping `tools:` in the first place: a denied name the other harness does not recognise denies
|
||||||
|
nothing, so a mis-shaped value can never grant or misroute a capability. The risk is a load failure,
|
||||||
|
never a silent over-grant. That asymmetry is why the same verbatim copy that makes `tools:`
|
||||||
|
unshippable makes `disallowedTools` worth shipping.
|
||||||
|
|
||||||
So the read-only orchestrator agents regain their write fence: `gitea-orchestrate`,
|
So the read-only orchestrator agents regain their write fence: `gitea-orchestrate`,
|
||||||
`apm-orchestrate` and `lint-runner` each carry `disallowedTools: Edit, Write, NotebookEdit` plus
|
`apm-orchestrate` and `lint-runner` each carry `disallowedTools: Edit, Write, NotebookEdit` plus
|
||||||
explicit prose in the body stating the agent does not edit files. `git-orchestrate` is deliberately
|
explicit prose in the body stating the agent does not edit files. `git-orchestrate` is deliberately
|
||||||
excluded — it legitimately declared `edit` before the conversion and still needs to write.
|
excluded — it legitimately declared `edit` before the conversion and still needs to write.
|
||||||
|
|
||||||
Net position: the allowlist stays dropped for the reason originally given; the specific capability
|
**Residual — the fence is partial, and the prose is doing more of the work than the field is.**
|
||||||
that mattered (preventing a read-only orchestrator from writing) is restored by the one mechanism
|
`disallowedTools: Edit, Write, NotebookEdit` denies exactly those three tools. It does not deny
|
||||||
that is portable by construction. The consequence below is narrowed accordingly.
|
`Bash`, and at plugin scope these agents carry no `tools:` and therefore inherit it, so
|
||||||
|
`bash -c 'echo … > f'` remains unfenced by frontmatter. Only the body prose covers that path. This
|
||||||
|
is not a regression introduced here — the pre-conversion `tools:` allowlists also granted `Bash`,
|
||||||
|
so the shell route was open then too — but the ADR should not credit the mechanism with more than
|
||||||
|
it delivers. Closing it would need a `disallowedTools` entry for `Bash`, which these agents cannot
|
||||||
|
take because they legitimately shell out.
|
||||||
|
|
||||||
|
Net position: the allowlist stays dropped for the reason originally given, and the denylist is
|
||||||
|
admitted as the portable-by-construction half of what was lost. It restores a real, Claude-Code-
|
||||||
|
confirmed write fence against the tool-call path, not a complete write sandbox. The consequence
|
||||||
|
below is narrowed accordingly.
|
||||||
|
|
||||||
Enforcement follows the decision: `agent-audit`'s plugin-scope validator reads its allowlist as
|
Enforcement follows the decision: `agent-audit`'s plugin-scope validator reads its allowlist as
|
||||||
data from the `apm-agent-allowlist` section of
|
data from the `apm-agent-allowlist` section of
|
||||||
@@ -126,7 +166,9 @@ whether a field is safe under verbatim copy in a single vendor-neutral file.
|
|||||||
lists from `references/field-inventory.md` rather than hardcoding them, with a `source_keys`
|
lists from `references/field-inventory.md` rather than hardcoding them, with a `source_keys`
|
||||||
provenance chain — survives and is reused. Only the *content shape* changes for plugin scope:
|
provenance chain — survives and is reused. Only the *content shape* changes for plugin scope:
|
||||||
`field-inventory.md` shifts from two side-by-side CC-only/Copilot-only blocklists to one
|
`field-inventory.md` shifts from two side-by-side CC-only/Copilot-only blocklists to one
|
||||||
vendor-neutral allowlist (`name`/`description`/`model`/`source_keys` — the last for provenance
|
vendor-neutral allowlist for plugin-scope agents, while continuing to serve its original
|
||||||
tracking, validated separately by `validate-provenance.sh` against `sources.md`, not a
|
two-blocklist role for project/user-scope validation. That file's `apm-agent-allowlist` section
|
||||||
provider-specific field) for plugin-scope agents, while
|
is the authoritative list and is read as data by `validate.sh`; as amended on 2026-08-14 it holds
|
||||||
continuing to serve its original two-blocklist role for project/user-scope validation.
|
`name`/`description`/`model`/`source_keys`/`disallowedTools` — `source_keys` for provenance
|
||||||
|
tracking, validated separately by `validate-provenance.sh` against `sources.md` rather than being
|
||||||
|
a provider-specific field, and `disallowedTools` per the amendment above.
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ plugin/APM scope, or a Claude Code and Copilot file pair at project/user scope.
|
|||||||
|
|
||||||
At **plugin/APM scope**, accepts the single `.apm/agents/<name>.agent.md` file — there is no
|
At **plugin/APM scope**, accepts the single `.apm/agents/<name>.agent.md` file — there is no
|
||||||
counterpart. Structural checks via `validate.sh` hard-`FAIL` any frontmatter field outside the
|
counterpart. Structural checks via `validate.sh` hard-`FAIL` any frontmatter field outside the
|
||||||
vendor-neutral allowlist (`name`, `description`, `model`, `source_keys` — the last for
|
vendor-neutral allowlist, since `apm compile` copies frontmatter verbatim to both harnesses and an
|
||||||
provenance tracking, checked separately by `validate-provenance.sh` against `sources.md`; see
|
unsafe field can't be silently dropped for just one of them. The allowlist itself lives in the
|
||||||
ADR-0016), since `apm compile`
|
`apm-agent-allowlist` section of `references/field-inventory.md` and is read from there as data —
|
||||||
copies frontmatter verbatim to both harnesses and an unsafe field can't be silently dropped for
|
consult that section rather than any restatement of it, including this one. As of 2026-08-14 it
|
||||||
just one of them.
|
admits `name`, `description`, `model`, `source_keys`, and `disallowedTools`; `source_keys` is
|
||||||
|
provenance metadata checked separately by `validate-provenance.sh` against `sources.md`, and
|
||||||
|
`disallowedTools` is admitted because a denylist survives verbatim copy where the `tools` allowlist
|
||||||
|
does not (ADR-0016 and its 2026-08-14 amendment).
|
||||||
|
|
||||||
At **project/user scope**, accepts either file in a CC `.md` / Copilot `.agent.md` pair, derives
|
At **project/user scope**, accepts either file in a CC `.md` / Copilot `.agent.md` pair, derives
|
||||||
the counterpart automatically, and validates both. Runs structural checks via `validate.sh`
|
the counterpart automatically, and validates both. Runs structural checks via `validate.sh`
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ Usage: validate.sh <agent-file>
|
|||||||
Validate an agent definition file against the agent definition spec.
|
Validate an agent definition file against the agent definition spec.
|
||||||
|
|
||||||
At plugin/APM scope, <agent-file> is a single vendor-neutral
|
At plugin/APM scope, <agent-file> is a single vendor-neutral
|
||||||
.apm/agents/<name>.agent.md file (frontmatter allowlist: name, description,
|
.apm/agents/<name>.agent.md file with no counterpart. Its frontmatter allowlist
|
||||||
model — no counterpart file). At project or user scope, <agent-file> is
|
is not restated here: it is read at load time from the apm-agent-allowlist
|
||||||
either half of a Claude Code .md / Copilot .agent.md pair.
|
section of references/field-inventory.md, which is the authoritative list.
|
||||||
|
At project or user scope, <agent-file> is either half of a Claude Code .md /
|
||||||
|
Copilot .agent.md pair.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
agent-file Path to the agent file (or either half of a project/user-scope pair).
|
agent-file Path to the agent file (or either half of a project/user-scope pair).
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ metadata:
|
|||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- At plugin/APM scope, bump the resolved package's `apm.yml` `version` after every change — minor for a new agent, patch for a fix. Consumers compare this version to detect updates; skipping it hides the change.
|
- At plugin/APM scope, bump the resolved package's `apm.yml` `version` after every change — minor for a new agent, patch for a fix. Consumers compare this version to detect updates; skipping it hides the change.
|
||||||
- At plugin/APM scope, `tools` and all Claude-only fields (`isolation`, `maxTurns`, `effort`, `memory`, `permissionMode`, `disallowedTools`, `skills`, `color`, `initialPrompt`, `background`, `hooks`, `mcpServers`) are omitted entirely, not merely restricted (ADR-0016: `apm compile` copies frontmatter verbatim to both harnesses with no per-target integrator, so a harness-specific value is wrong on at least one). Only project/user scope supports these fields.
|
- At plugin/APM scope, `tools` and all Claude-only fields (`isolation`, `maxTurns`, `effort`, `memory`, `permissionMode`, `skills`, `color`, `initialPrompt`, `background`, `hooks`, `mcpServers`) are omitted entirely, not merely restricted (ADR-0016: `apm compile` copies frontmatter verbatim to both harnesses with no per-target integrator, so a harness-specific value is wrong on at least one). Only project/user scope supports these fields.
|
||||||
|
- `disallowedTools` is the one exception, on **shape**, not favouritism. `tools` is an *allowlist* whose vocabulary differs per harness (Claude tool names vs Copilot's `execute`/`read`/`edit`/`search`/`agent`/`web`), so verbatim copy makes one value wrong on one target. A *denylist* cannot fail that way: an unrecognised name denies nothing, so the worst case is a missing fence, never a wrong grant. Claude Code honours it for plugin subagents — `docs/research/docs/claude-code-plugins/agent-definition.md:99` lists the three fields plugin agents ignore (`hooks`, `mcpServers`, `permissionMode`) and this is not one. Write it on every read-only plugin-scope agent (ADR-0016's 2026-08-14 amendment).
|
||||||
|
- That fence is partial: it denies only the tools it names. It does not deny `Bash`, which a plugin-scope agent with no `tools` inherits, so a shell redirect still writes. Say the agent is read-only in the body too.
|
||||||
- An `apm.yml` with no top-level `type:` field is a marketplace-only manifest, not a package root — the walk-up skips it and keeps going.
|
- An `apm.yml` with no top-level `type:` field is a marketplace-only manifest, not a package root — the walk-up skips it and keeps going.
|
||||||
- `AskUserQuestion`, `EnterPlanMode`, `ExitPlanMode`, `ScheduleWakeup`, and `WaitForMcpServers` are never available to any subagent regardless of the `tools` field. Exception: `ExitPlanMode` is available when the parent session runs in `permissionMode: plan`.
|
- `AskUserQuestion`, `EnterPlanMode`, `ExitPlanMode`, `ScheduleWakeup`, and `WaitForMcpServers` are never available to any subagent regardless of the `tools` field. Exception: `ExitPlanMode` is available when the parent session runs in `permissionMode: plan`.
|
||||||
- Duplicate `name` values in the same scope: Claude Code silently discards one without warning. Always verify uniqueness before shipping.
|
- Duplicate `name` values in the same scope: Claude Code silently discards one without warning. Always verify uniqueness before shipping.
|
||||||
@@ -90,7 +92,7 @@ The script is file-by-file no-op — it skips any file that already exists.
|
|||||||
|
|
||||||
### Step 2 — Fill in the agent file(s)
|
### Step 2 — Fill in the agent file(s)
|
||||||
|
|
||||||
**At plugin/APM scope**, there is exactly one file: `<package-root>/.apm/agents/<name>.agent.md`. Frontmatter carries ONLY `name`, `description`, optionally `model`, and optionally `source_keys` (provenance metadata, not a runtime field — see the template) — never `tools` or the other Claude-only fields listed in Gotchas (ADR-0016). Fill in `name`, `description`, `model`, and the system prompt body per the guidance below; the rest of this step's field-by-field guidance (tools, maxTurns, effort, memory, isolation, disallowedTools, skills, color, initialPrompt, background) is project/user scope only. Skip Step 3 and go to Step 4.
|
**At plugin/APM scope**, there is exactly one file: `<package-root>/.apm/agents/<name>.agent.md`. Its frontmatter allowlist is the `apm-agent-allowlist` section of `agent-audit`'s `references/field-inventory.md`, read from there as data: `name`, `description`, `model`, `source_keys` (provenance metadata, not a runtime field — see the template), and `disallowedTools` for a read-only agent. Never `tools` or the other Claude-only fields listed in Gotchas (ADR-0016). Fill those in plus the system prompt body per the guidance below; the rest of this step's field-by-field guidance is project/user scope only. Skip Step 3 and go to Step 4.
|
||||||
|
|
||||||
**At project/user scope**, continue below to fill in both provider files — this step covers the Claude Code file (`<name>.md`); Step 3 covers the Copilot file.
|
**At project/user scope**, continue below to fill in both provider files — this step covers the Claude Code file (`<name>.md`); Step 3 covers the Copilot file.
|
||||||
|
|
||||||
@@ -106,13 +108,14 @@ Open the scaffolded Claude Code file. Replace every `FILL IN:` placeholder. **Re
|
|||||||
|
|
||||||
**`tools`** (project/user scope only — never at plugin/APM scope) — restrict to what the agent actually needs. Omit to inherit all tools. Use `Agent(type1,type2)` to limit which subagent types this agent can spawn; omit `Agent` entirely to prevent spawning.
|
**`tools`** (project/user scope only — never at plugin/APM scope) — restrict to what the agent actually needs. Omit to inherit all tools. Use `Agent(type1,type2)` to limit which subagent types this agent can spawn; omit `Agent` entirely to prevent spawning.
|
||||||
|
|
||||||
**Optional fields worth considering (project/user scope only — never at plugin/APM scope):**
|
**`disallowedTools`** (all scopes, including plugin/APM) — denylist applied before `tools` and taking precedence over it; supports `mcp__<server>`, `mcp__<server>__*`, and `mcp__*` globs. `api-reference.md:40` types it `string / list` and `agent-definition.md:71` types it `string[]`, so a YAML list or a delimited string both work; this repo's plugin-scope agents use the comma-separated string (`disallowedTools: Edit, Write, NotebookEdit`) — match that.
|
||||||
|
|
||||||
|
**Optional fields worth considering (project/user scope only — never at plugin/APM scope, with the exception of `model`, which is allowed at every scope):**
|
||||||
- `model`: set when this agent needs a different capability tier (`haiku` for fast tasks, `opus` for deep reasoning)
|
- `model`: set when this agent needs a different capability tier (`haiku` for fast tasks, `opus` for deep reasoning)
|
||||||
- `maxTurns`: set a cap to prevent runaway agents on bounded tasks
|
- `maxTurns`: set a cap to prevent runaway agents on bounded tasks
|
||||||
- `effort`: set to `low` for single-lookup tasks, `high` or above for deep reasoning or multi-file analysis — overrides session effort level; omit to inherit
|
- `effort`: set to `low` for single-lookup tasks, `high` or above for deep reasoning or multi-file analysis — overrides session effort level; omit to inherit
|
||||||
- `memory`: `user`, `project`, or `local` — only when cross-session state is genuinely needed
|
- `memory`: `user`, `project`, or `local` — only when cross-session state is genuinely needed
|
||||||
- `isolation: worktree` — only when the agent modifies files and needs an isolated copy
|
- `isolation: worktree` — only when the agent modifies files and needs an isolated copy
|
||||||
- `disallowedTools`: space-separated denylist applied before `tools`; supports `mcp__*` glob patterns (e.g. `disallowedTools: mcp__filesystem__*`)
|
|
||||||
- `skills`: list of skill names preloaded at agent startup — different from the `source_keys` metadata field
|
- `skills`: list of skill names preloaded at agent startup — different from the `source_keys` metadata field
|
||||||
- `color`: UI color for the agent tile (`red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan`)
|
- `color`: UI color for the agent tile (`red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan`)
|
||||||
- `initialPrompt`: auto-submitted as the first turn when this agent activates as the main session thread; only set when this agent is intended for main-thread activation
|
- `initialPrompt`: auto-submitted as the first turn when this agent activates as the main session thread; only set when this agent is intended for main-thread activation
|
||||||
@@ -152,7 +155,7 @@ Skip this step entirely at plugin/APM scope — there is no separate Copilot fil
|
|||||||
|
|
||||||
**`source_keys`** — add the same top-level list as the CC file when research sources were used. Omit when no research was used.
|
**`source_keys`** — add the same top-level list as the CC file when research sources were used. Omit when no research was used.
|
||||||
|
|
||||||
**Remove all template documentation comments from the YAML frontmatter after filling in required fields** — these are marked with `<!--` and `-->` and must be deleted before shipping.
|
**Delete the `<!-- -->` template comments from the frontmatter**, as in Step 2.
|
||||||
|
|
||||||
The system prompt body should match the Claude Code version — the agent's task definition is the same across providers.
|
The system prompt body should match the Claude Code version — the agent's task definition is the same across providers.
|
||||||
|
|
||||||
@@ -183,37 +186,32 @@ If no research sources are in context, delete `sources.md`.
|
|||||||
|
|
||||||
### Step 5 — Validate and close
|
### Step 5 — Validate and close
|
||||||
|
|
||||||
Run this checklist before invoking the audit:
|
Run this checklist before invoking the audit.
|
||||||
|
|
||||||
|
**Every file, at every scope:**
|
||||||
|
- [ ] `name` present and kebab-case; `description` present
|
||||||
|
- [ ] System prompt body present and non-empty
|
||||||
|
- [ ] No `FILL IN:` placeholders and no `<!-- -->` template comments remain
|
||||||
|
|
||||||
**Plugin/APM scope — single file (`<name>.agent.md`):**
|
**Plugin/APM scope — single file (`<name>.agent.md`):**
|
||||||
- [ ] `name` field present, kebab-case, unique in scope
|
- [ ] `name` unique in scope; `description` action-first
|
||||||
- [ ] `description` field present and action-first
|
- [ ] Every frontmatter field is in the `apm-agent-allowlist` section of `agent-audit`'s `references/field-inventory.md` — the single source of truth, read as data by `validate.sh`. As of 2026-08-14: `name`, `description`, `model`, `source_keys`, `disallowedTools`. Nothing else — in particular no `tools`
|
||||||
- [ ] Frontmatter contains ONLY `name`, `description`, and optionally `model` (plus `source_keys` if research-sourced) — no `tools`, `isolation`, `maxTurns`, `effort`, `memory`, `permissionMode`, `disallowedTools`, `skills`, `color`, `initialPrompt`, `background`, `hooks`, or `mcpServers`
|
- [ ] A read-only agent carries `disallowedTools` **and** says so in the body
|
||||||
- [ ] System prompt body present and non-empty
|
|
||||||
- [ ] No `FILL IN:` placeholders remain
|
|
||||||
- [ ] No `<!-- -->` template comments remain in frontmatter
|
|
||||||
|
|
||||||
**Project/user scope — Claude Code file (`<name>.md`):**
|
**Project/user scope — Claude Code file (`<name>.md`):**
|
||||||
- [ ] `name` field present, kebab-case, unique in scope
|
- [ ] `name` unique in scope; `description` action-first
|
||||||
- [ ] `description` field present and action-first
|
|
||||||
- [ ] System prompt body present and non-empty
|
|
||||||
- [ ] No `FILL IN:` placeholders remain
|
|
||||||
- [ ] No `<!-- -->` template comments remain in frontmatter
|
|
||||||
|
|
||||||
**Project/user scope — Copilot CLI file (`<name>.agent.md`):**
|
**Project/user scope — Copilot CLI file (`<name>.agent.md`):**
|
||||||
- [ ] File extension is `.agent.md` (not `.md`)
|
- [ ] File extension is `.agent.md` (not `.md`)
|
||||||
- [ ] `name` field matches the filename stem (e.g. `name: my-agent` in `my-agent.agent.md`)
|
- [ ] `name` matches the filename stem (e.g. `name: my-agent` in `my-agent.agent.md`)
|
||||||
- [ ] `description` field present
|
|
||||||
- [ ] No Claude Code-only fields (`maxTurns`, `isolation`, `memory`, `permissionMode`, `effort`, `hooks`, `mcpServers`)
|
- [ ] No Claude Code-only fields (`maxTurns`, `isolation`, `memory`, `permissionMode`, `effort`, `hooks`, `mcpServers`)
|
||||||
- [ ] System prompt body present and non-empty
|
|
||||||
- [ ] Body does not exceed 30,000 characters
|
- [ ] Body does not exceed 30,000 characters
|
||||||
- [ ] No `<!-- -->` template comments remain in frontmatter
|
|
||||||
|
|
||||||
At plugin/APM scope, apply a **minor bump** to the resolved package's `apm.yml` `version` (single manifest, e.g. `1.0.4` → `1.1.0`).
|
At plugin/APM scope, apply a **minor bump** to the resolved package's `apm.yml` `version` (single manifest, e.g. `1.0.4` → `1.1.0`).
|
||||||
|
|
||||||
Invoke `kyberforge:agent-audit` on the created file(s) before closing — validates the pair at project/user scope, the single file at plugin/APM scope.
|
Invoke `kyberforge:agent-audit` on the created file(s) before closing — validates the pair at project/user scope, the single file at plugin/APM scope.
|
||||||
|
|
||||||
**Commit verification.** Capture `git log --oneline -1` before Step 1 and keep it. Once the audit is clean, run `git add` and `git commit` for the new agent files — do not stop at staging. Then run `git log --oneline -1` again and confirm the hash changed from the one you captured at the start. A non-empty `git diff --stat` is not sufficient proof of completion: staged-but-uncommitted work isn't part of any commit and can be silently lost if the working tree is cleaned up before a commit lands. Only report the agent as done once the hash has actually changed.
|
**Commit verification.** Once the audit is clean, run `git add` and `git commit` for the new agent files — do not stop at staging. Then confirm `git log --oneline -1` differs from the hash captured before Step 1. A non-empty `git diff --stat` is not proof of completion: staged-but-uncommitted work is part of no commit and can be silently lost if the working tree is cleaned up. Only report the agent as done once the hash has actually changed.
|
||||||
|
|
||||||
## Improving an existing agent
|
## Improving an existing agent
|
||||||
|
|
||||||
@@ -223,9 +221,7 @@ Confirm the agent files exist and at least one improvement signal is present in
|
|||||||
|
|
||||||
If no signals: "This skill applies existing signals to an agent. For a blind review, examine the files manually or run a grill session first."
|
If no signals: "This skill applies existing signals to an agent. For a blind review, examine the files manually or run a grill session first."
|
||||||
|
|
||||||
Verify `kyberforge:agent-audit` is available — it ships with the kyberforge plugin and is co-installed with this skill. If unavailable, stop and tell the user to install the kyberforge plugin before continuing.
|
Verify `kyberforge:agent-audit` is available, as in the create flow's Prerequisites. Capture `git log --oneline -1` now, before making any edits — Step 5 needs it to verify a real commit landed.
|
||||||
|
|
||||||
Capture `git log --oneline -1` now, before making any edits — Step 5 needs it to verify a real commit landed.
|
|
||||||
|
|
||||||
**Partial state (project/user scope only)** — if one provider file exists but not the other, scaffold the missing one (`bash scripts/new-agent.sh <name> <root>`, file-by-file no-op) then continue. Doesn't apply at plugin/APM scope — single file, no partial-pair state.
|
**Partial state (project/user scope only)** — if one provider file exists but not the other, scaffold the missing one (`bash scripts/new-agent.sh <name> <root>`, file-by-file no-op) then continue. Doesn't apply at plugin/APM scope — single file, no partial-pair state.
|
||||||
|
|
||||||
@@ -248,7 +244,7 @@ Before editing, state which root causes were identified, what evidence supports
|
|||||||
|
|
||||||
### Step 4 — Apply changes
|
### Step 4 — Apply changes
|
||||||
|
|
||||||
Edit any file the signals point to. Generalize the fix — find the underlying gap, not the specific example that failed. For every sentence you add, ask: "Would the agent get this wrong without it?" A shorter, focused definition consistently outperforms an exhaustive one. For Copilot files, verify no Claude Code-only fields are introduced. For a plugin/APM-scope single file, verify no field beyond `name`, `description`, `model`, and `source_keys` is introduced.
|
Edit any file the signals point to. Generalize the fix — find the underlying gap, not the specific example that failed. For every sentence you add, ask: "Would the agent get this wrong without it?" A shorter, focused definition consistently outperforms an exhaustive one. For Copilot files, verify no Claude Code-only fields are introduced. For a plugin/APM-scope single file, verify every field is still in the `apm-agent-allowlist` section of `agent-audit`'s `references/field-inventory.md`, and that an existing `disallowedTools` fence was not dropped by the edit.
|
||||||
|
|
||||||
If the edit adds or removes research-sourced content, update `source_keys` in the edited file(s) and the corresponding entry in `sources.md` per Create flow's Step 4.
|
If the edit adds or removes research-sourced content, update `source_keys` in the edited file(s) and the corresponding entry in `sources.md` per Create flow's Step 4.
|
||||||
|
|
||||||
@@ -260,4 +256,4 @@ At plugin/APM scope, apply a **patch bump** to the resolved package's `apm.yml`
|
|||||||
|
|
||||||
Invoke `kyberforge:agent-audit` on the edited file(s) to confirm no regressions — the pair at project/user scope, the single file at plugin/APM scope.
|
Invoke `kyberforge:agent-audit` on the edited file(s) to confirm no regressions — the pair at project/user scope, the single file at plugin/APM scope.
|
||||||
|
|
||||||
**Commit verification.** Capture `git log --oneline -1` at the start of Step 1 and keep it. Once the audit is clean, run `git add` and `git commit` for the changed files — do not stop at staging. Then run `git log --oneline -1` again and confirm the hash changed from the one you captured at the start. A non-empty `git diff --stat` is not sufficient proof of completion: staged-but-uncommitted work isn't part of any commit and can be silently lost if the working tree is cleaned up before a commit lands. Only report the improvement as done once the hash has actually changed.
|
**Commit verification.** Exactly as in the create flow's Step 5, against the hash captured at Step 1: commit the changed files once the audit is clean, and only report the improvement as done once `git log --oneline -1` shows a different hash.
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ Annotated agent definition templates copied by `scripts/new-agent.sh` when scaff
|
|||||||
|
|
||||||
- **`claude-code.md`** — Claude Code agent definition template (project/user scope). Includes all supported frontmatter fields (required and optional) with inline guidance comments and `FILL IN:` placeholders.
|
- **`claude-code.md`** — Claude Code agent definition template (project/user scope). Includes all supported frontmatter fields (required and optional) with inline guidance comments and `FILL IN:` placeholders.
|
||||||
- **`copilot.agent.md.template`** — Copilot CLI agent definition template (CLI format, project/user scope). Excludes cloud/IDE-only fields (`target`, `user-invocable`, `disable-model-invocation`, `mcp-servers`) and Claude Code-only fields. Uses Copilot tool aliases (`execute`, `read`, `edit`, `search`, `agent`, `web`).
|
- **`copilot.agent.md.template`** — Copilot CLI agent definition template (CLI format, project/user scope). Excludes cloud/IDE-only fields (`target`, `user-invocable`, `disable-model-invocation`, `mcp-servers`) and Claude Code-only fields. Uses Copilot tool aliases (`execute`, `read`, `edit`, `search`, `agent`, `web`).
|
||||||
- **`apm-agent.md`** — Vendor-neutral APM agent definition template (plugin/APM scope). Only `name`, `description`, optional `model`, and optional `source_keys` (provenance metadata, not a runtime field) in frontmatter — no `tools` and no Claude-only fields, since `apm compile` copies frontmatter verbatim to both the Claude Code and Copilot CLI targets with no per-target integrator (ADR-0016).
|
- **`apm-agent.md`** — Vendor-neutral APM agent definition template (plugin/APM scope). Frontmatter is limited to the `apm-agent-allowlist` section of `agent-audit`'s `references/field-inventory.md` — the authoritative list, read from there as data by `agent-audit`'s `validate.sh`; this file deliberately does not restate it. No `tools` and no Claude-only knobs, since `apm compile` copies frontmatter verbatim to both the Claude Code and Copilot CLI targets with no per-target integrator; `disallowedTools` is scaffolded as an opt-in comment because a denylist, unlike the `tools` allowlist, survives that copy (ADR-0016 and its 2026-08-14 amendment).
|
||||||
|
|||||||
@@ -2,18 +2,25 @@
|
|||||||
<!-- Vendor-neutral APM agent definition (plugin/APM scope).
|
<!-- Vendor-neutral APM agent definition (plugin/APM scope).
|
||||||
Path: <package-root>/.apm/agents/<name>.agent.md — one file, no counterpart.
|
Path: <package-root>/.apm/agents/<name>.agent.md — one file, no counterpart.
|
||||||
`apm compile` copies this frontmatter verbatim to BOTH the Claude Code and
|
`apm compile` copies this frontmatter verbatim to BOTH the Claude Code and
|
||||||
Copilot CLI targets — there is no per-target field integrator. Claude's
|
Copilot CLI targets, with no per-target field integrator to reconcile
|
||||||
`tools:` (space-separated string) and Copilot's `tools:` (alias list) are
|
anything, so a harness-specific value is wrong on at least one of them.
|
||||||
incompatible vocabularies, and Claude-only fields (isolation, maxTurns,
|
|
||||||
effort, memory, permissionMode) have no Copilot equivalent. A value correct
|
|
||||||
for one harness is guaranteed wrong on the other, so this scope carries
|
|
||||||
ONLY the fields below — full stop (see ADR-0016). `source_keys` is
|
|
||||||
provenance metadata, not a runtime field, and is exempt from that rule.
|
|
||||||
|
|
||||||
Do NOT add: tools, isolation, maxTurns, effort, memory, permissionMode,
|
This template does not restate the permitted-field list. The authoritative
|
||||||
disallowedTools, skills, color, initialPrompt, background, hooks, or
|
list is the `apm-agent-allowlist` section of agent-audit's
|
||||||
mcpServers. Omitting `tools` means inherit-all-tools on both harnesses,
|
references/field-inventory.md, which agent-audit's validate.sh reads from
|
||||||
which is never wrong.
|
there as data — a list copied into a template goes stale one step further
|
||||||
|
out than the list itself. Every field scaffolded below is on it; before
|
||||||
|
adding any other field, check that section.
|
||||||
|
|
||||||
|
The shape rule behind the list (ADR-0016 and its 2026-08-14 amendment):
|
||||||
|
`tools` is an ALLOWLIST whose vocabulary differs per harness — Claude tool
|
||||||
|
names vs Copilot's execute/read/edit/search/agent/web — so one value is
|
||||||
|
wrong on one target. Never add it here; omitting it means inherit-all-tools
|
||||||
|
on both harnesses, which is never wrong. `disallowedTools` is a DENYLIST
|
||||||
|
and is allowed for exactly that reason: a name the other harness does not
|
||||||
|
recognise denies nothing, so the worst case is a missing fence, never a
|
||||||
|
wrongly granted capability. Claude-only knobs (isolation, maxTurns, effort,
|
||||||
|
memory, permissionMode) have no Copilot equivalent and stay out.
|
||||||
|
|
||||||
Fill in all FILL IN: placeholders. Delete template comments before shipping. -->
|
Fill in all FILL IN: placeholders. Delete template comments before shipping. -->
|
||||||
|
|
||||||
@@ -30,6 +37,16 @@ description: FILL IN: Action-first description of what this agent does and when
|
|||||||
Optional. Aliases: sonnet, opus, haiku, fable. Or full model ID.
|
Optional. Aliases: sonnet, opus, haiku, fable. Or full model ID.
|
||||||
Omit to inherit the runtime default on whichever harness compiles this file. -->
|
Omit to inherit the runtime default on whichever harness compiles this file. -->
|
||||||
|
|
||||||
|
<!-- disallowedTools: Edit, Write, NotebookEdit
|
||||||
|
Optional. Denylist, applied before `tools` and taking precedence over it.
|
||||||
|
Add it when this agent is read-only — it is the one tool restriction that
|
||||||
|
survives verbatim copy (see the header comment). Claude Code honours it for
|
||||||
|
plugin subagents — confirmed. Copilot's handling of the key is unconfirmed;
|
||||||
|
ADR-0016 accepts that as a stated risk rather than a settled fact.
|
||||||
|
It denies only the tools it names. It does NOT deny Bash, which this agent
|
||||||
|
inherits, so a shell redirect still writes — state the read-only boundary
|
||||||
|
in the system prompt body as well, not in frontmatter alone. -->
|
||||||
|
|
||||||
<!-- source_keys:
|
<!-- source_keys:
|
||||||
- slug-name
|
- slug-name
|
||||||
Development-only. Add when research sources informed this agent (slugs must match
|
Development-only. Add when research sources informed this agent (slugs must match
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ description: FILL IN: Action-first description of what this agent does and when
|
|||||||
Be specific about the triggering condition and domain.
|
Be specific about the triggering condition and domain.
|
||||||
Example: "Reviews pull request diffs for security issues. Use proactively after code changes." -->
|
Example: "Reviews pull request diffs for security issues. Use proactively after code changes." -->
|
||||||
|
|
||||||
<!-- tools: Read Bash Grep
|
<!-- tools: Read, Bash, Grep
|
||||||
Optional. Space-separated allowlist. Omit to inherit all tools from parent.
|
Optional. Allowlist of tool names: a comma-separated string or a YAML list.
|
||||||
|
Omit to inherit all tools from parent.
|
||||||
Use Agent(type1,type2) to restrict which subagent types this agent can spawn.
|
Use Agent(type1,type2) to restrict which subagent types this agent can spawn.
|
||||||
Omit Agent entirely to prevent this agent from spawning subagents.
|
Omit Agent entirely to prevent this agent from spawning subagents.
|
||||||
Never available to subagents regardless of tools field:
|
Never available to subagents regardless of tools field:
|
||||||
@@ -47,9 +48,13 @@ description: FILL IN: Action-first description of what this agent does and when
|
|||||||
<!-- background: false
|
<!-- background: false
|
||||||
Optional. Set true to force background execution. -->
|
Optional. Set true to force background execution. -->
|
||||||
|
|
||||||
<!-- disallowedTools: mcp__filesystem__write_file
|
<!-- disallowedTools: Edit, Write, NotebookEdit
|
||||||
Optional. Space-separated denylist, applied before the tools allowlist.
|
Optional. Denylist, applied before the tools allowlist and taking precedence over it.
|
||||||
Supports mcp__* glob patterns (e.g. mcp__filesystem__* to block all filesystem tools). -->
|
Accepts a YAML list or a delimited string; use the comma-separated string form for
|
||||||
|
consistency with the plugin-scope agents in this repo.
|
||||||
|
Supports mcp__* glob patterns (e.g. mcp__filesystem__* to block all filesystem tools).
|
||||||
|
Denies only the tools it names — it does not deny Bash, so an agent that inherits
|
||||||
|
Bash can still write via a shell redirect. State read-only intent in the body too. -->
|
||||||
|
|
||||||
<!-- skills:
|
<!-- skills:
|
||||||
- skill-name
|
- skill-name
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ source_keys: []
|
|||||||
|
|
||||||
## deployment-modes.md
|
## deployment-modes.md
|
||||||
|
|
||||||
Agent scope hierarchy, precedence rules, and per-scope restrictions. Covers: which fields are silently ignored for plugin agents (Claude Code and Copilot CLI), scoped identifiers for plugin subdirectory agents, cache isolation behaviour, and Copilot CLI path conventions. Loaded conditionally from SKILL.md when the destination is a plugin directory.
|
Agent scope hierarchy, precedence rules, and per-scope restrictions. Covers: which frontmatter fields a plugin/APM-scope agent may carry and the allowlist-vs-denylist shape rule that decides it (deferring to `agent-audit`'s `references/field-inventory.md` for the list itself), scoped identifiers for plugin subdirectory agents, cache isolation behaviour, and Copilot CLI path conventions. Loaded conditionally from SKILL.md when the destination is a plugin directory.
|
||||||
|
|
||||||
## scripts.md
|
## scripts.md
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ When the same agent `name` appears at multiple scopes, **user scope wins over pr
|
|||||||
|
|
||||||
## Plugin scope restrictions
|
## Plugin scope restrictions
|
||||||
|
|
||||||
Plugin/APM agents (`.apm/agents/<name>.agent.md`) carry only `name`, `description`, optionally `model`, and optionally `source_keys` (provenance metadata, not a runtime field — silently ignored by both harnesses) in frontmatter — full stop (see ADR-0016). `apm compile` copies this frontmatter verbatim to both the Claude Code and Copilot CLI compile targets with no per-target integrator: Claude's `tools:` (space-separated string) and Copilot's `tools:` (alias list) are incompatible vocabularies, and Claude-only fields have no Copilot equivalent, so any harness-specific value is guaranteed wrong on at least one target.
|
Plugin/APM agents (`.apm/agents/<name>.agent.md`) carry only the fields in the `apm-agent-allowlist` section of `agent-audit`'s `references/field-inventory.md`. That section is the authoritative list — `agent-audit`'s `validate.sh` reads it from there as data, and it changes — so consult it rather than any restatement of it. `apm compile` copies this frontmatter verbatim to both the Claude Code and Copilot CLI compile targets with no per-target integrator, so a harness-specific value is guaranteed wrong on at least one target (ADR-0016).
|
||||||
|
|
||||||
This makes the old "silently ignored at plugin scope" framing moot. It's not that `hooks`, `mcpServers`, `permissionMode`, `tools`, `isolation`, `maxTurns`, `effort`, `memory`, `disallowedTools`, `skills`, `color`, `initialPrompt`, or `background` are merely ignored at this scope — they are never written to the file at all. Copy the agent to `.claude/agents/` (project scope) or `~/.claude/agents/` (user scope) to use any of them.
|
**The rule is about a field's shape, not a fixed roster.** `tools` is an **allowlist** whose vocabulary differs per harness — Claude Code names its own tools, Copilot CLI uses aliases (`execute`/`read`/`edit`/`search`/`agent`/`web`) — so under verbatim copy one value is wrong on one target. It stays out. `disallowedTools` is a **denylist**, and denying by name has no such conflict: a name the other harness does not recognise denies nothing, so the worst case is that the fence is absent there, never that a capability is wrongly granted. That asymmetry is why the denylist is admitted where the allowlist is not (ADR-0016's 2026-08-14 amendment). Claude Code honours it for plugin subagents — `docs/research/docs/claude-code-plugins/agent-definition.md:99` names the three fields plugin agents silently ignore (`hooks`, `mcpServers`, `permissionMode`) and `disallowedTools` is not among them. It is a partial fence: it denies only the tools it names, not `Bash`, which a plugin-scope agent with no `tools` inherits — so state read-only intent in the body too.
|
||||||
|
|
||||||
|
This makes the old "silently ignored at plugin scope" framing moot for the excluded fields. It's not that `hooks`, `mcpServers`, `permissionMode`, `tools`, `isolation`, `maxTurns`, `effort`, `memory`, `skills`, `color`, `initialPrompt`, or `background` are merely ignored at this scope — they are never written to the file at all. Copy the agent to `.claude/agents/` (project scope) or `~/.claude/agents/` (user scope) to use any of them.
|
||||||
|
|
||||||
## Scoped identifiers (Claude Code plugin agents only)
|
## Scoped identifiers (Claude Code plugin agents only)
|
||||||
|
|
||||||
|
|||||||
@@ -77,15 +77,29 @@ teardown() {
|
|||||||
assert_failure
|
assert_failure
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "plugin/APM scope: frontmatter carries only name, description, model, source_keys fields" {
|
# The permitted set is read from the same data agent-audit's validate.sh reads --
|
||||||
|
# the apm-agent-allowlist section of agent-audit's field-inventory.md -- rather than
|
||||||
|
# restated here. A hardcoded copy drifts: this assertion listed four fields and went
|
||||||
|
# on passing after ADR-0016's amendment added disallowedTools, and would have
|
||||||
|
# rejected a scaffolded agent that legitimately carried it.
|
||||||
|
@test "plugin/APM scope: frontmatter carries only allowlisted fields" {
|
||||||
|
inventory="$BATS_TEST_DIRNAME/../../agent-audit/references/field-inventory.md"
|
||||||
|
[ -f "$inventory" ] || fail "field-inventory.md not found at $inventory"
|
||||||
|
allowlist="$(awk '
|
||||||
|
/^## apm-agent-allowlist$/ { insection = 1; next }
|
||||||
|
insection && /^##/ { exit }
|
||||||
|
insection && NF && $0 !~ /^#/ && $0 !~ /^---/ { print; exit }
|
||||||
|
' "$inventory")"
|
||||||
|
[ -n "$allowlist" ] || fail "apm-agent-allowlist section is empty in $inventory"
|
||||||
|
|
||||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||||
bash "$SCRIPT" my-agent "$ROOT"
|
bash "$SCRIPT" my-agent "$ROOT"
|
||||||
file="$ROOT/.apm/agents/my-agent.agent.md"
|
file="$ROOT/.apm/agents/my-agent.agent.md"
|
||||||
fm="$(sed -n '/^---$/,/^---$/p' "$file")"
|
fm="$(sed -n '/^---$/,/^---$/p' "$file")"
|
||||||
keys="$(grep -oE '^[a-zA-Z][a-zA-Z0-9_-]*:' <<< "$fm" | sed 's/:$//' | sort -u)"
|
keys="$(grep -oE '^[a-zA-Z][a-zA-Z0-9_-]*:' <<< "$fm" | sed 's/:$//' | sort -u)"
|
||||||
for key in $keys; do
|
for key in $keys; do
|
||||||
if [[ "$key" != "name" && "$key" != "description" && "$key" != "model" && "$key" != "source_keys" ]]; then
|
if ! grep -qw "$key" <<< "$allowlist"; then
|
||||||
fail "unexpected frontmatter key: $key"
|
fail "frontmatter key '$key' is not in field-inventory.md's apm-agent-allowlist ($allowlist)"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,13 @@ mirrored file; the `check-plugin-content-sync` pre-push hook reports it as drift
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Supported events: `PreToolUse`, `PostToolUse`, `Notification`, `Stop`.
|
Events (**partial list**): `PreToolUse`, `PostToolUse`, `Notification`, `Stop`. Claude Code's plugin
|
||||||
|
hook set is larger — `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `PreCompact` and
|
||||||
|
`SubagentStop` also exist — and this repo's vendored corpus does not enumerate it anywhere:
|
||||||
|
`docs/research/docs/claude-code-plugins/configuration.md:100` describes the file as "Event handlers
|
||||||
|
(PreToolUse, PostToolUse, etc.)", and `agent-definition.md:53` covers only the per-agent `hooks`
|
||||||
|
field, not the plugin-level set. Treat the four names above as the ones this repo has verified, not
|
||||||
|
as the schema. Check Claude Code's own hooks documentation before wiring an event not listed here.
|
||||||
|
|
||||||
Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin — the plugin runs from a cache
|
Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin — the plugin runs from a cache
|
||||||
path after install, not its original repo location.
|
path after install, not its original repo location.
|
||||||
@@ -47,8 +53,37 @@ lowercase and differently named (`sessionStart`, `sessionEnd`, `userPromptSubmit
|
|||||||
`postToolUse`, `errorOccurred`, `agentStop`). See
|
`postToolUse`, `errorOccurred`, `agentStop`). See
|
||||||
`docs/research/docs/github-copilot-plugins/configuration.md`.
|
`docs/research/docs/github-copilot-plugins/configuration.md`.
|
||||||
|
|
||||||
There is no separate Copilot hooks file at this plugin root. An earlier revision of this document
|
There is no separate Copilot hooks file at this plugin root, and — as things stand — **Copilot
|
||||||
pointed at `hooks.json` at the plugin root for Copilot; that file was a stale artifact of the
|
resolves to no hooks file at all.** Two corrections to an earlier revision of this document, which
|
||||||
pre-ADR-0017 sync and was deleted. Both ecosystems now resolve to the single generated
|
got both halves of this wrong:
|
||||||
`hooks/hooks.json`, which is currently empty (`{"hooks": {}}`) — the two schemas would need
|
|
||||||
reconciling before any real hook ships to both.
|
**The deleted root `hooks.json` was not a stale sync artifact.** `plugins/kyberforge/hooks.json` was
|
||||||
|
added in `2287ddc` (2026-06-20), the commit that created the plugin, well before
|
||||||
|
`scripts/sync-plugin-content.sh` existed; `plugins/lint/hooks.json` arrived the same way in
|
||||||
|
`f326df4`. Main's Copilot manifest `plugins/kyberforge/plugin.json` declared `"hooks": "hooks.json"`,
|
||||||
|
and `plugins/lint/plugin.json` did the same — these were deliberately pointed-at Copilot hooks files,
|
||||||
|
not leftovers. The sync (`38f1ba4`) later took ownership of that path, and ADR-0017's 2026-08-14
|
||||||
|
amendment moved the generated file to `hooks/hooks.json` because that, not the plugin root, is the
|
||||||
|
path Claude Code convention-scans.
|
||||||
|
|
||||||
|
**Only Claude Code resolves to `hooks/hooks.json`.** Claude Code finds it by auto-discovery.
|
||||||
|
Copilot does not: `docs/research/docs/github-copilot-plugins/configuration.md:47` types `hooks` as a
|
||||||
|
`plugin.json` field of type "string or object" with **no default**, so there is no convention path to
|
||||||
|
scan, and `jq 'has("hooks")'` returns `false` for all six `.github/plugin/plugin.json` files that
|
||||||
|
`apm pack` emits. With the pointer gone and no auto-discovery to fall back on, the Copilot ecosystem
|
||||||
|
sees zero hooks.
|
||||||
|
|
||||||
|
The effect is the twin of the `mcpServers` gap that ADR-0017's 2026-08-13 amendment re-injects for:
|
||||||
|
same "string or object" type, same absence of a default, same outcome of a Copilot manifest with no
|
||||||
|
pointer. The *mechanism* differs, and ADR-0017 is explicit about it — `mcpServers` is actively
|
||||||
|
stripped by `build_plugin_manifest`, whereas `hooks` "was never in `build_plugin_manifest`'s strip
|
||||||
|
list at all"; it is simply never emitted, because `apm.yml` has no key that produces one. So this is
|
||||||
|
an absence apm never fills, not a removal to reverse. It is harmless **today only** because both
|
||||||
|
generated files are empty (`{"hooks": {}}`), so nothing is being lost. The first real hook authored
|
||||||
|
under `.apm/hooks/` would ship to Claude Code and silently not to Copilot.
|
||||||
|
|
||||||
|
Follow-up, not fixed here: decide whether `sync-plugin-content.sh` should re-inject a `hooks` pointer
|
||||||
|
into `.github/plugin/plugin.json` the way `reinject_mcp_servers()` already does for `mcpServers`. That
|
||||||
|
decision has a prerequisite — the two hook schemas differ (see above), so a single `hooks/hooks.json`
|
||||||
|
pointed at from both manifests would hand Copilot a file shaped for Claude Code. Reconcile the schemas
|
||||||
|
before wiring the pointer.
|
||||||
|
|||||||
Reference in New Issue
Block a user