docs: source ADR-0029 claims and sync ADRs and hook docs with behaviour
- cite the VS Code prompt-file deprecation and the verbatim apm quote - add ADR-0029 boundary-clause enforcement and Consequences - mark superseded ADR-0019 passages; record neutral lock advice, source fork and reloadSkills, amend for the hook hardening - move the ADR-0025 amendment out of the Decision list - amend ADR-0022 for create keeping 0.1.0 - fix hooks.md merge and event claims, README guard caveat, gates.md Vale globs, and pin the research registry URL Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
This commit is contained in:
16
CONTEXT.md
16
CONTEXT.md
@@ -67,9 +67,10 @@ consumers, but a deliberate choice, never a default. A rule for this repo alone
|
||||
_Avoid_: rule (the Claude-side deployed form under `.claude/rules/`), guideline, standard
|
||||
|
||||
**Hook**:
|
||||
A runtime callback a harness fires inside its own tool loop, authored as one JSON file per concern
|
||||
under `plugins/<plugin>/.apm/hooks/` in apm's canonical shape — nested entries, PascalCase events,
|
||||
`${PLUGIN_ROOT}` script paths — which apm renders per target. Reach is narrowed in the package's
|
||||
A runtime callback a harness fires inside its own tool loop, authored as JSON under
|
||||
`plugins/<plugin>/.apm/hooks/` — one file or several; kyberforge ships a single `hooks.json` — in
|
||||
apm's canonical shape — nested entries, PascalCase events, `${PLUGIN_ROOT}` script paths — which
|
||||
apm renders per target. Reach is narrowed in the package's
|
||||
`apm.yml` `targets:`, never by filename. The last resort among apm primitives: procedure belongs in
|
||||
a **Skill**, and a hook is only for "this must always happen at this event".
|
||||
_Avoid_: trigger, callback script (the script is the hook's payload, not the hook)
|
||||
@@ -225,7 +226,8 @@ _Avoid_: namespace, category
|
||||
recheck belongs to `factory-audit`, not to `forge`, which routes only to the author
|
||||
skills and never to an audit.
|
||||
- "prompt" meant both apm's `.prompt.md` primitive and, loosely, any slash command or a skill — resolved:
|
||||
a **Prompt** is the `.prompt.md` primitive under the house rule above. apm calls a prompt "a
|
||||
callable program for an LLM", but on Claude it deploys as a model-invocable command with fewer
|
||||
frontmatter keys than a skill, and Codex receives nothing. A fat prompt is a worse skill on every
|
||||
harness, so the procedure goes in the skill and the prompt only steers it.
|
||||
a **Prompt** is the `.prompt.md` primitive under the house rule above. apm frames a prompt as a
|
||||
program ("a prompt is a program for an LLM", its "What is APM?" page), but on Claude it deploys
|
||||
as a model-invocable command with fewer frontmatter keys than a skill, and Codex receives
|
||||
nothing. A fat prompt is a worse skill on every harness, so the procedure goes in the skill and
|
||||
the prompt only steers it.
|
||||
|
||||
@@ -34,7 +34,9 @@ behind and refreshes it in place.
|
||||
*session* loads, and that is the moment the staleness does damage. It also enables two things a git
|
||||
hook structurally cannot do: `additionalContext` puts the notice into the agent's context rather
|
||||
than terminal scrollback nobody reads, and `reloadSkills: true` makes the host re-scan the skill
|
||||
directories after the hook returns, so a refresh lands in the running session without a restart.
|
||||
directories after the hook returns, so a refresh lands in the running session without a restart
|
||||
(`reloadSkills` is a documented `SessionStart` `hookSpecificOutput` field:
|
||||
code.claude.com/docs/en/hooks, checked 2026-09-29).
|
||||
|
||||
apm's own lifecycle events (`pre-/post-install`, `pre-/post-update`, `pre-/post-uninstall`) were
|
||||
rejected: they fire around apm operations already chosen, so they can announce a refresh but never
|
||||
@@ -51,8 +53,9 @@ Three sub-decisions:
|
||||
drift. A hook shipped in a package is written into that file by apm itself, so it is apm's output
|
||||
and does not drift. `.claude/settings.local.json` also works but is gitignored and machine-local,
|
||||
which fails the requirement that this travel with the repo.
|
||||
- **`startup` matcher only.** `resume`, `clear`, `compact` and `fork` would re-run the check on every
|
||||
compaction, and a compaction is not an event after which the remote can have moved.
|
||||
- **`startup` matcher only.** `resume`, `clear`, `compact` and `fork` — the other documented
|
||||
`SessionStart` matchers (code.claude.com/docs/en/hooks, checked 2026-09-29) — would re-run the check
|
||||
on every compaction, and a compaction is not an event after which the remote can have moved.
|
||||
|
||||
The executable-trust gate is switched on at the same time. Root `apm.yml` gains an `executables:`
|
||||
block allowing kyberforge's hooks and bin.
|
||||
@@ -142,6 +145,22 @@ value to be larger — so changing either side without the other fails the suite
|
||||
> Re-measured: ~24–26 s for the same six-behind refresh, warm, on a LAN remote — well inside the
|
||||
> 380 s above.
|
||||
|
||||
> **Amendment (2026-09-29) — the time limits are harder to escape, and portable.** Four changes to
|
||||
> `check-apm-current.sh` (PR #144):
|
||||
>
|
||||
> - Both limits are now `timeout -k 5`: a child that ignores SIGTERM is SIGKILLed 5 s later. The
|
||||
> worst case is 60 + 5 + 300 + 5 = 370 s, still below the host's 380, and the test now sums each
|
||||
> limit plus its grace.
|
||||
> - `timeout` falls back to `gtimeout` (Homebrew coreutils on macOS). With neither on `PATH` the
|
||||
> hook emits a notice and exits without calling `apm`. Before, the missing binary exited 127, the
|
||||
> `|| exit 0` swallowed it, and the hook silently never ran.
|
||||
> - `GIT_TERMINAL_PROMPT=0` is exported, so a remote that wants credentials fails at once instead of
|
||||
> blocking on an invisible prompt until the timeout fires.
|
||||
> - `apm update` runs under `flock -n` on `apm_modules/.kyberforge-apm-update.lock` when `flock` exists
|
||||
> and `apm_modules/` does. A second session starting at the same moment skips its refresh and says
|
||||
> so, instead of running a second `apm update` over the same tree. Without `flock` (stock macOS) the
|
||||
> refresh runs unserialised, as before.
|
||||
|
||||
**Reading a human-readable CLI for a control decision cost a silent failure, again.** `apm outdated`
|
||||
has no `--json` or other machine-readable flag (confirmed against 0.28.0), so the hook must match
|
||||
its prose. The first attempt matched `outdated dependencies found` — plural only. apm emits
|
||||
@@ -204,9 +223,20 @@ Until then the repo has the mechanism in source and not in effect.
|
||||
> and it does not last. At the next session start the hook finds the restored lock behind `main`
|
||||
> and refreshes again.
|
||||
|
||||
> **Amendment (2026-09-19) — the advice is neutral when the default branch is unknown.** The hook
|
||||
> picks its lock advice by comparing the current branch with `refs/remotes/origin/HEAD`. That ref is
|
||||
> often unset: git writes it on clone, and `git remote add` never does. The fallback used to be
|
||||
> `main`, which told a checkout whose default branch is `master` to discard a real lock update while
|
||||
> it stood on its default branch. Now, when `origin/HEAD` is unset, the notice gives neither the
|
||||
> default-branch nor the feature-branch advice. It says only "commit it or discard it
|
||||
> deliberately", the same text used outside a git checkout or on a detached HEAD. Learning the
|
||||
> remote's default would need the network, so the hook asserts nothing and the reader decides
|
||||
> (`ea119d8`).
|
||||
|
||||
**`.claude/settings.json` stops being `{"hooks": {}}`.** apm merges the hook into it and tracks
|
||||
ownership in a `.claude/apm-hooks.json` sidecar, with the script copied to
|
||||
`.claude/hooks/<pkg>/`. The sidecar and the script directory are gitignored install output; the
|
||||
`.claude/hooks/<pkg>/`. The sidecar and the script directory are gitignored install output
|
||||
(*superseded for the sidecar: it is committed, see correction 2026-09-16 below*); the
|
||||
settings file remains committed, now with apm-generated content in it. ADR-0018's statement that the
|
||||
committed content is exactly `{"hooks": {}}` is superseded on that point only — the rule it was
|
||||
protecting, that nothing repo-authored goes in that file, is unchanged.
|
||||
@@ -223,7 +253,9 @@ protecting, that nothing repo-authored goes in that file, is unchanged.
|
||||
|
||||
**Native consumers are protected by a guard, not by the gate.** A host installing holocron through
|
||||
`claude plugin install` auto-discovers `hooks/hooks.json` and does not consult apm's trust gate at
|
||||
all. The script therefore exits silently when there is no `apm.lock.yaml` in the working directory,
|
||||
all. The script therefore exits silently when there is no `apm.lock.yaml` in the working directory
|
||||
(*superseded: it now checks `${CLAUDE_PROJECT_DIR}/apm.lock.yaml` and has no cwd fallback, see
|
||||
correction 2026-09-28 below*),
|
||||
which is what makes it inert in a repo that does not consume packages through apm. Copilot CLI sees
|
||||
no hook at all, for the reasons already documented in `plugins/kyberforge/docs/hooks.md`.
|
||||
|
||||
|
||||
@@ -191,6 +191,17 @@ comments (`:9-93`) have described all three correctly since it shipped.
|
||||
was wrong for the tree-identical case: the `same_subtree` skip makes a push **pass** that this ADR as
|
||||
written requires to **fail**, which is documented behaviour changing, not an implementation detail.
|
||||
|
||||
## Amendment (2026-09-29): a new skill stays at `0.1.0` until its first improve
|
||||
|
||||
The Decision says `skill-author`'s create/improve bump convention is "unchanged". It has changed
|
||||
since. Before, `skill-author` bumped the **minor** version on create, so a skill left its first
|
||||
session above the `0.1.0` scaffold. Now create keeps the scaffold's `0.1.0` and does not bump it;
|
||||
the first improve is the first bump, a **patch** (`skill-author` `SKILL.md`, `references/create.md`
|
||||
and `references/improve.md`; `965208b`, PR #144). This makes `0.1.0` mean what this ADR says it
|
||||
means, "created and never yet revised", instead of a value no created skill ever kept. The rules
|
||||
above are otherwise unaffected: `metadata.version` is still required, and the push gate still
|
||||
compares whatever value is there.
|
||||
|
||||
## Consequences
|
||||
|
||||
27 SKILL.md files gain `metadata.version: "1.0.0"`, and a 28th — `bin/write-docs` — reaches the same
|
||||
|
||||
@@ -172,31 +172,6 @@ fallback row. It could not route a `SKILL.md` file path, a trigger its own descr
|
||||
Its agent row ("a path under `.apm/agents/`… or an agent markdown file") was both wider than the
|
||||
script and circular.
|
||||
|
||||
> **Amendment (2026-09-28) — three more modes: hook, instruction and prompt.** The "two accepted
|
||||
> shapes" above are now five. The `primitive-author` change (issue #94, PR #144) added three rows to
|
||||
> the Step 0 table, each with its own self-contained flow file and a shared
|
||||
> `scripts/lib-checks-primitive.sh` that `validate.sh` sources for all three:
|
||||
>
|
||||
> - A file named `*.instructions.md` takes the instruction flow (`references/instruction-flow.md`).
|
||||
> - A file named `*.prompt.md` takes the prompt flow (`references/prompt-flow.md`); the prompt rules
|
||||
> themselves are ADR-0029's.
|
||||
> - A `.json` file whose *immediate* parent directory is `hooks/` (`.apm/hooks/`, or a package's
|
||||
> root `hooks/`) takes the hook flow (`references/hook-flow.md`).
|
||||
>
|
||||
> The suffix rows are checked before the `agents/`-parent rule, so a `*.prompt.md` or
|
||||
> `*.instructions.md` under `agents/` is not audited as an agent. The fallback row is unchanged in
|
||||
> kind: anything else stops, runs no validator, exits 2, and names every accepted shape rather than
|
||||
> two. The exit-code table below applies to the new modes as written; its "matches neither shape"
|
||||
> row now means "matches none of the five".
|
||||
>
|
||||
> This is ADR-0020's merge-siblings rule applied forward rather than a new decision. Auditing a hook,
|
||||
> an instruction or a prompt is the same job as auditing a skill or agent — deterministic checks,
|
||||
> a read, a qualitative pass, one shared report — over a different input type, which is exactly the
|
||||
> case the rule says belongs in one skill behind a dispatch table, not in a new sibling audit skill.
|
||||
> The authoring side follows the rule's other half: the author skills stay split because they emit
|
||||
> genuinely different artifacts, so the three primitives got their own `primitive-author` rather
|
||||
> than rows in `skill-author` or `agent-author`.
|
||||
|
||||
**3. One entry point per script, auto-detecting, with the mode-specific half sourced.**
|
||||
|
||||
- `scripts/validate.sh` detects the target type itself, then sources `scripts/lib-boundary-resolver.sh`
|
||||
@@ -303,6 +278,31 @@ both entry points, and that is what most of the table is. Every change below is
|
||||
A single `validate.sh` copied or symlinked out of its `scripts/` directory still does not work,
|
||||
because its libraries are not beside it. It now fails at exit 2 and says so.
|
||||
|
||||
> **Amendment (2026-09-28) — three more modes: hook, instruction and prompt.** The "two accepted
|
||||
> shapes" in point 2 above are now five. The `primitive-author` change (issue #94, PR #144) added
|
||||
> three rows to the Step 0 table, each with its own self-contained flow file and a shared
|
||||
> `scripts/lib-checks-primitive.sh` that `validate.sh` sources for all three:
|
||||
>
|
||||
> - A file named `*.instructions.md` takes the instruction flow (`references/instruction-flow.md`).
|
||||
> - A file named `*.prompt.md` takes the prompt flow (`references/prompt-flow.md`); the prompt rules
|
||||
> themselves are ADR-0029's.
|
||||
> - A `.json` file whose *immediate* parent directory is `hooks/` (`.apm/hooks/`, or a package's
|
||||
> root `hooks/`) takes the hook flow (`references/hook-flow.md`).
|
||||
>
|
||||
> The suffix rows are checked before the `agents/`-parent rule, so a `*.prompt.md` or
|
||||
> `*.instructions.md` under `agents/` is not audited as an agent. The fallback row is unchanged in
|
||||
> kind: anything else stops, runs no validator, exits 2, and names every accepted shape rather than
|
||||
> two. The exit-code table above applies to the new modes as written; its "matches neither shape"
|
||||
> row now means "matches none of the five".
|
||||
>
|
||||
> This is ADR-0020's merge-siblings rule applied forward rather than a new decision. Auditing a hook,
|
||||
> an instruction or a prompt is the same job as auditing a skill or agent — deterministic checks,
|
||||
> a read, a qualitative pass, one shared report — over a different input type, which is exactly the
|
||||
> case the rule says belongs in one skill behind a dispatch table, not in a new sibling audit skill.
|
||||
> The authoring side follows the rule's other half: the author skills stay split because they emit
|
||||
> genuinely different artifacts, so hooks, instructions and prompts got their own
|
||||
> `primitive-author` rather than rows in `skill-author` or `agent-author`.
|
||||
|
||||
## Considered options
|
||||
|
||||
**Keep two skills and rely on the byte-identity contract test alone (rejected).** This is the status
|
||||
|
||||
@@ -8,17 +8,21 @@ parameters. It is the text a user would otherwise type again and again. It carri
|
||||
beyond steering existing skills or agents by name. Once it holds reusable know-how, bundled files,
|
||||
or anything the model should find on its own, it is a **Skill** in the wrong container.
|
||||
|
||||
This is stricter than apm. apm's docs call a prompt "a callable program for an LLM", and 0.28.0
|
||||
scaffolds one as a numbered-steps workflow (`apm_cli/workflow/discovery.py`). On every harness this
|
||||
repo targets, a prompt with a full workflow in it is a worse skill:
|
||||
This is stricter than apm. apm frames a prompt as a program: its docs state that "a prompt is a
|
||||
program for an LLM" (microsoft.github.io/apm/concepts/what-is-apm/, "Secure by default"; the same
|
||||
sentence is in the apm-cli 0.28.0 package README), and 0.28.0 scaffolds one as a numbered-steps
|
||||
workflow (`apm_cli/workflow/discovery.py`). On every harness this repo targets, a prompt with a full
|
||||
workflow in it is a worse skill:
|
||||
|
||||
- **Claude Code.** Custom commands have been merged into skills. "Both create `/deploy` and work
|
||||
the same way", and both are model-invocable by default (code.claude.com/docs/en/skills.md).
|
||||
apm 0.28.0 keeps only `description`, `allowed-tools`, `model`, `argument-hint` and `input` for
|
||||
Claude (`_PRESERVED_COMMAND_KEYS`) and drops `disable-model-invocation`. A deployed prompt is
|
||||
therefore a model-visible skill with fewer frontmatter keys, and it cannot be made user-only.
|
||||
- **Copilot / VS Code.** Prompt files are marked deprecated for Agent Host, and VS Code offers a
|
||||
migration to agent skills.
|
||||
- **Copilot / VS Code.** "Prompt files are deprecated for Agent Host sessions and aren't loaded by
|
||||
Agent Host", and VS Code offers a migration that converts existing prompt files to agent skills
|
||||
(code.visualstudio.com/docs/agent-customization/prompt-files). They still load in the Local agent,
|
||||
which that page says will be removed in a future release.
|
||||
- **Codex.** Codex receives no prompts at all.
|
||||
|
||||
The one job a prompt does better than a skill is a short, parameterised "do this now, using X and Y"
|
||||
@@ -45,10 +49,28 @@ happen, the cost is bounded: the prompt is a thin wrapper that calls the right s
|
||||
- `description` is present and non-empty (FAIL).
|
||||
- It is 250 characters or fewer (SUGGESTION).
|
||||
- It has no "Use when" trigger clause (SUGGESTION).
|
||||
- It has no `Not X -> Y` boundary clause (SUGGESTION).
|
||||
- **`factory-audit`, judgment step.** A body that clearly carries reusable procedure is a FAIL. A
|
||||
borderline body is a SUGGESTION. There is deliberately no line-count or heading heuristic: the
|
||||
call is made by reading the content, because any threshold misfires.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **`factory-audit` gains a prompt mode.** `*.prompt.md` is a Step 0 dispatch row with its own
|
||||
`references/prompt-flow.md`, and `scripts/lib-checks-primitive.sh` carries the three description
|
||||
checks above plus the judgment step (ADR-0025, amendment 2026-09-28). A prompt that was fine under
|
||||
apm's framing — a trigger clause, a boundary clause, a numbered workflow — now draws findings.
|
||||
- **`primitive-author` refuses procedure-bearing prompts.** Its prompt reference opens with the
|
||||
boundary gate, so a "make me a command" request that carries reusable know-how is redirected to
|
||||
`skill-author` before any file is written. That makes skill-vs-prompt a checked boundary rather
|
||||
than an authoring preference.
|
||||
- **Codex gets no prompts from this repo.** apm deploys none to Codex, so anything a prompt steers
|
||||
must also be reachable there through the skill it names. Keeping procedure in the skill is what
|
||||
makes that true; a fat prompt would be content Codex users silently never see.
|
||||
- **Reversing this is cheap in files, not in routing.** No prompt exists in the repo yet, so the rule
|
||||
constrains new work only. Loosening it later means re-deciding the description contract, and every
|
||||
prompt written under it would need a trigger clause added.
|
||||
|
||||
## Considered options
|
||||
|
||||
- **Fat workflow prompts as peers of skills (rejected).** This follows apm's framing. But every
|
||||
|
||||
@@ -787,8 +787,10 @@ Wiring Vale as a deterministic prefilter for `factory-audit`'s Description dimen
|
||||
issue #84) is repo-specific, not part of the generic `lint` plugin, so it does not live in
|
||||
`plugins/lint/` — and per ADR-0014 it no longer lives at the repo root either. It lives **once**,
|
||||
under `plugins/kyberforge/.apm/skills/factory-audit/assets/vale/`, carrying both the `Kyberforge`
|
||||
and `KyberforgeCopilot` styles and a single `.vale.ini` with all three glob sections:
|
||||
`[**/SKILL.md]`, `[**/agents/*.md]`, `[**/*.agent.md]`.
|
||||
and `KyberforgeCopilot` styles and a single `.vale.ini` with five glob sections:
|
||||
`[**/SKILL.md]`, `[**/agents/*.md]`, `[**/*.instructions.md]`, `[**/*.prompt.md]` and
|
||||
`[**/*.agent.md]`. The instructions and prompt sections arrived with `factory-audit`'s primitive
|
||||
modes (ADR-0025, amendment 2026-09-28).
|
||||
|
||||
ADR-0014 split this into two skill-scoped copies because a plugin's cache-install copies only each
|
||||
skill's own files and `skill-audit` could not reach across the skill boundary into `agent-audit`'s
|
||||
@@ -816,7 +818,8 @@ Its `StylesPath` and `BasedOnStyles` checks were **not** diffs. They were per-fi
|
||||
invoked `vale --config` on one representative path per file shape. It was the only assertion
|
||||
anywhere that catches a `.vale.ini` glob typo (`[**/SKILL.md]` → `[**/SKILLS.md]`), the failure mode
|
||||
where every other check stays clean while Vale lints zero files. One config does not make that
|
||||
impossible: a typo in any one of the three sections still 0-file-skips that shape.
|
||||
impossible: a typo in any one section still 0-file-skips that shape. The table has since grown
|
||||
to eight rows: one each for the instructions and prompt sections added later.
|
||||
|
||||
**Case 0** runs before any Vale-dependent case and needs no Vale binary. It asserts that the shipped
|
||||
`.vale.ini` exists and is readable, sets a `StylesPath` that resolves to a directory, and names only
|
||||
@@ -826,8 +829,9 @@ cases back.
|
||||
|
||||
The probes now live in `tests/test-vale-wrap.sh` (cases 28–30), rehomed against the merged config:
|
||||
one representative path per file shape, each asserted to produce a Vale scan of more than zero files
|
||||
*and* a Kyberforge alert (case 28). Case 28 also checks that each probe path is in scope of a
|
||||
published vale hook, and that every `.vale.ini` section has a probe row. Its Part B drops
|
||||
*and* a Kyberforge alert (case 28). Case 28 also checks that every `.vale.ini` section has an
|
||||
isolating probe row. It no longer holds each probe path to a hook's `files:` scope: that half read
|
||||
the retired `.pre-commit-hooks.yaml`, and case 32 owns the local hooks' scope. Its Part B drops
|
||||
`Kyberforge` from each section's `BasedOnStyles` in a copy and requires that section's probes to
|
||||
fail as "style not loaded". Case 29 is a mutation case: it typos each section in a copy of the
|
||||
assets and requires that section's isolating probes to drop to zero. Case 30 asserts that
|
||||
@@ -931,17 +935,17 @@ authors. Without the binary the hooks fail with a bare "command not found" and n
|
||||
**Two hooks, not one combined hook — for a different reason than ADR-0014 gave.** The original
|
||||
reason was mechanical: with a config per skill, a single hook could point at only one copy and would
|
||||
silently 0-file-skip the other file shape (see
|
||||
[A 0-file Vale run is NOT RUN](#a-0-file-vale-run-is-not-run)). One `.vale.ini` carrying all three
|
||||
sections removes that constraint. The split stays anyway because the `files:` regexes still have to
|
||||
[A 0-file Vale run is NOT RUN](#a-0-file-vale-run-is-not-run)). One `.vale.ini` carrying every
|
||||
section removes that constraint. The split stays anyway because the `files:` regexes still have to
|
||||
differ — each hook hands Vale only the file shape it is scoped to. Both hooks name the same
|
||||
plugin-bundled `factory-audit/scripts/vale-wrap.sh` through `repo: local`; there is no second root
|
||||
copy.
|
||||
|
||||
### The `.vale.ini` globs do no scoping
|
||||
|
||||
The `.vale.ini`'s section globs are **path-agnostic** — `[**/SKILL.md]`, `[**/agents/*.md]` and
|
||||
`[**/*.agent.md]` — and constrain filename *shape*, not
|
||||
location: Vale's `*` crosses `/`. A `SKILL.md` outside `plugins/` (a project-scope
|
||||
The `.vale.ini`'s section globs are **path-agnostic** — `[**/SKILL.md]`, `[**/agents/*.md]`,
|
||||
`[**/*.instructions.md]`, `[**/*.prompt.md]` and `[**/*.agent.md]` — and constrain filename *shape*,
|
||||
not location: Vale's `*` crosses `/`. A `SKILL.md` outside `plugins/` (a project-scope
|
||||
`.claude/skills/foo/SKILL.md`, say) still matches `[**/SKILL.md]` and gets linted normally.
|
||||
|
||||
All scoping therefore comes from the pre-commit hooks' own `files:` regexes, which pin this repo's
|
||||
@@ -951,8 +955,16 @@ invocation — in this repo or in any repo that installs it, whatever that repo'
|
||||
Narrowing a `.vale.ini` glob to a `plugins/`-shaped path to "tighten" it breaks the consumer case:
|
||||
`factory-audit` run against a project-scope `.claude/skills/` tree would lint nothing.
|
||||
`check-vale-style-sync`'s probe set was built to catch exactly that; it moved to
|
||||
`tests/test-vale-wrap.sh` with the hook's deletion, and two of the six probes exist specifically to
|
||||
pin this location independence — see [One copy, one config](#one-copy-one-config).
|
||||
`tests/test-vale-wrap.sh` with the hook's deletion. Its table (`PROBE_TABLE28`) now has eight rows,
|
||||
one or more per section, and the two `.claude/`-prefixed rows exist specifically to pin this
|
||||
location independence — see [One copy, one config](#one-copy-one-config).
|
||||
|
||||
**No pre-commit Vale hook covers `*.instructions.md` or `*.prompt.md` yet.** The `.vale.ini`
|
||||
sections exist so that `factory-audit`'s own Vale call lints those files when it is handed one, in
|
||||
any repo. This repo's two prefilter hooks select only `SKILL.md` and `.agent.md` files, and the repo
|
||||
has no instruction or prompt file for a third hook to select. Case 32 requires every Vale hook's
|
||||
`files:` regex to match at least one tracked file, so a hook added now would fail it. Add the hook
|
||||
in the change that lands the first `.apm/instructions/` or `.apm/prompts/` file.
|
||||
|
||||
### The blind spot: `references/` is unlinted, for two independent reasons
|
||||
|
||||
@@ -1051,9 +1063,9 @@ clean.
|
||||
### Pre-push
|
||||
|
||||
`vale` is still a **pre-push** dependency, but no longer through a hook of its own.
|
||||
`check-vale-style-sync` — the hook that ran the six glob probes, and whose
|
||||
`check-vale-style-sync` — the hook that ran the original six glob probes, and whose
|
||||
`CHECK_VALE_STYLE_SYNC_ALLOW_MISSING_VALE=1` opt-out downgraded them audibly rather than skipping the
|
||||
hook — is deleted with the second Vale copy (ADR-0025). The six glob probes survive it inside
|
||||
hook — is deleted with the second Vale copy (ADR-0025). The glob probes survive it inside
|
||||
`test-vale-wrap.sh`, so `run-tests --strict` is now the gate that runs them. That is also what keeps
|
||||
`vale` a pre-push requirement: `test-vale-wrap.sh` exits 77 without the binary once its static cases
|
||||
pass, and a skip fails the push.
|
||||
|
||||
@@ -33,7 +33,7 @@ Authoring source lives in `.apm/`; it is the only content source and the only th
|
||||
| Agents | `.apm/agents/*.agent.md` | Role-based agents; one vendor-neutral `.agent.md` per agent (ADR-0016) |
|
||||
| Hooks | `.apm/hooks/` | Event-triggered automation — authored for Claude Code, see below |
|
||||
|
||||
**Hooks are authored for Claude Code, but apm writes them for every package target.** apm merges `.apm/hooks/*.json` into the consuming project's `.claude/settings.json` at install. Because kyberforge also targets Copilot and Codex, apm writes the same hook to `.github/hooks/kyberforge-hooks.json` (nested shape passed through, not reshaped) and into `.codex/hooks.json` when `.codex/` exists. Whether those harnesses execute it is unverified; if they do, it exits immediately, because the script exits 0 unless `CLAUDE_PROJECT_DIR`, which Claude Code exports for SessionStart hooks, is set. Details are in `docs/hooks.md` and ADR-0019's 2026-09-28 amendment and correction.
|
||||
**Hooks are authored for Claude Code, but apm writes them for every package target.** apm merges `.apm/hooks/*.json` into the consuming project's `.claude/settings.json` at install. Because kyberforge also targets Copilot and Codex, apm writes the same hook to `.github/hooks/kyberforge-hooks.json` (nested shape passed through, not reshaped) and into `.codex/hooks.json` when `.codex/` exists. Whether those harnesses execute it is unverified; if they do, it exits immediately, because the script exits 0 unless `CLAUDE_PROJECT_DIR` is set, and of the three only Claude Code documents exporting it for SessionStart hooks (unless the variable is inherited from the user's environment). Details are in `docs/hooks.md` and ADR-0019's 2026-09-28 amendment and correction.
|
||||
|
||||
## Skills
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ Author hooks in `plugins/kyberforge/.apm/hooks/*.json`. `.apm/` is the only cont
|
||||
`apm install` is the only supported install path (ADR-0024) — there is no generated mirror at the
|
||||
plugin root and no per-plugin `plugin.json`, so `.apm/hooks/` is both where you edit and what ships.
|
||||
|
||||
apm merges every `*.json` in that directory into a single hook definition and writes the event
|
||||
bindings into the consuming project's `.claude/settings.json`; see "Deployed shape" below. Scripts a
|
||||
hook invokes live in the same directory, alongside the JSON that references them.
|
||||
For Claude, apm merges the event bindings from every `*.json` in that directory into the consuming
|
||||
project's `.claude/settings.json`; see "Deployed shape" below. That merge is Claude's rendering, not
|
||||
a general rule: Copilot gets one file per source file (see "GitHub Copilot CLI and Codex"). Scripts
|
||||
a hook invokes live in the same directory, alongside the JSON that references them.
|
||||
|
||||
## Hook file structure
|
||||
|
||||
@@ -32,14 +33,12 @@ The shape Claude Code reads, and therefore the shape to author under `.apm/hooks
|
||||
}
|
||||
```
|
||||
|
||||
Events (**partial list**): `PreToolUse`, `PostToolUse`, `Notification`, `Stop`, and `SessionStart`
|
||||
(verified end-to-end by the hook below). Claude Code's plugin hook set is larger — `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 five 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.
|
||||
Events: see `plugins/kyberforge/docs/research/docs/microsoft-apm/hooks-primitive-schema.md`,
|
||||
section "Events: `_HOOK_EVENT_MAP`", for which names apm renames per target and which it passes
|
||||
through unchanged. For Claude, author every event in PascalCase (`SessionStart`, `UserPromptSubmit`,
|
||||
`PreCompact`, and so on). A camelCase name apm does not map, such as `userPromptSubmit`, draws only
|
||||
a non-fatal warning and never fires; an all-lowercase one draws no warning at all and never fires
|
||||
either.
|
||||
|
||||
## Referencing a script — use the `.apm/` path
|
||||
|
||||
@@ -85,7 +84,9 @@ mechanic. See ADR-0019, correction 2026-09-19, and the comment above `executable
|
||||
|
||||
`check-apm-current.sh` keeps an apm-consumed install level with its remote: it runs `apm outdated`,
|
||||
and if anything is behind, runs `apm update --yes` and returns `reloadSkills: true` so the running
|
||||
session picks up the redeployed content. Rationale, measurements, and the failure modes are in
|
||||
session picks up the redeployed content. `reloadSkills` is a documented `SessionStart`
|
||||
`hookSpecificOutput` field that makes Claude Code re-scan skill directories once the hooks finish
|
||||
(code.claude.com/docs/en/hooks, checked 2026-09-29). Rationale, measurements, and the failure modes are in
|
||||
ADR-0019.
|
||||
|
||||
**Claude Code only, and where it looks for the lockfile.** The hook exits 0 at once, silently and
|
||||
@@ -101,12 +102,26 @@ host that sets no `CLAUDE_PROJECT_DIR` the lockfile guard passes in every apm co
|
||||
fallback ran `apm update --yes` there (ADR-0019, correction 2026-09-28).
|
||||
|
||||
**The `timeout` in `hooks.json` must exceed the script's own budget.** The script spends at most
|
||||
`timeout 60 apm outdated` plus `timeout 300 apm update`; the hook entry declares `timeout: 380`, the
|
||||
sum plus a buffer. Set it lower and a slow remote gets the hook SIGKILLed mid-`apm update`, leaving a
|
||||
`timeout -k 5 60 apm outdated` plus `timeout -k 5 300 apm update`, 370 s counting each 5 s SIGKILL
|
||||
grace; the hook entry declares `timeout: 380`, the sum plus a buffer. Set it lower and a slow remote gets the hook SIGKILLed mid-`apm update`, leaving a
|
||||
partially redeployed `.claude/skills/` and emitting no notice — precisely the silent failure the hook
|
||||
exists to prevent. `tests/test-apm-current-hook.sh` pins the relationship (host timeout > sum of the
|
||||
script's internal timeouts) rather than the literal, so raising either side alone fails the suite.
|
||||
|
||||
**The time limits are portable and hard to escape (ADR-0019, amendment 2026-09-29).** The script
|
||||
uses `timeout`, or `gtimeout` where only Homebrew coreutils provides it (stock macOS). With neither,
|
||||
it emits a notice and exits without running `apm`, instead of dying silently on exit 127. It exports
|
||||
`GIT_TERMINAL_PROMPT=0`, so a remote that wants credentials fails at once instead of waiting out the
|
||||
timeout on a prompt nobody can see. Where `flock` exists and `apm_modules/` does too, `apm update`
|
||||
runs under a non-blocking lock on `apm_modules/.kyberforge-apm-update.lock`. A second session that
|
||||
starts during a refresh skips its own and says so. Without `flock` the refresh runs unserialised.
|
||||
|
||||
**The lock advice is neutral when the default branch is unknown.** The notice tells you to discard
|
||||
the rewritten `apm.lock.yaml` on a feature branch and to decide deliberately on the default branch.
|
||||
It learns the default from `refs/remotes/origin/HEAD`, which `git remote add` never writes. When
|
||||
that ref is unset, on a detached HEAD, or outside a git checkout, it says only "commit it or
|
||||
discard it deliberately" rather than guessing `main` (ADR-0019, amendment 2026-09-19).
|
||||
|
||||
**Staleness is detected by matching apm's summary line, and both spellings count.** `apm outdated`
|
||||
has no `--json` or otherwise machine-readable output (verified against apm 0.28.0), so the hook
|
||||
greps its text. apm prints `1 outdated dependency found` in the singular when exactly one package is
|
||||
|
||||
@@ -4,7 +4,7 @@ source_keys:
|
||||
- apm-cli-installed-source
|
||||
- apm-docs-llms-full
|
||||
- apm-github-repo
|
||||
- context7-microsoft-apm
|
||||
- context7-microsoft-apm # partial: earlier pass only; the 2026-09-28 re-verification could not reach Context7 (see sources.md)
|
||||
---
|
||||
|
||||
Ground truth for this file is the installed apm-cli **0.28.0** source (`apm_cli/integration/hook_integrator.py`, `hook_native_formats.py`, `hook_ir.py`, `hook_file_routing.py`, `_hook_dropped_targets.py`, `targets.py`, `security/executables.py`) plus a live `apm install` of a scratch package targeting `claude` and `copilot` (2026-09-28). Where the published docs (`llms-full.txt`) disagree with 0.28.0, the disagreement is called out; the published docs track upstream `main` and may describe a newer release.
|
||||
|
||||
@@ -4,7 +4,7 @@ source_keys:
|
||||
- apm-cli-installed-source
|
||||
- apm-docs-llms-full
|
||||
- apm-github-repo
|
||||
- context7-microsoft-apm
|
||||
- context7-microsoft-apm # partial: earlier pass only; the 2026-09-28 re-verification could not reach Context7 (see sources.md)
|
||||
---
|
||||
|
||||
This file is checked against the installed apm-cli **0.28.0** source (`primitives/models.py`, `primitives/parser.py`, `primitives/discovery.py`, `utils/patterns.py`, `integration/instruction_integrator.py`, `integration/base_integrator.py`, `integration/targets.py`, `compilation/agents_compiler.py`, `commands/compile/cli.py`) and against a live `apm install` / `apm compile` of a scratch package (2026-09-28).
|
||||
|
||||
@@ -4,7 +4,7 @@ source_keys:
|
||||
- apm-cli-installed-source
|
||||
- apm-docs-llms-full
|
||||
- apm-github-repo
|
||||
- context7-microsoft-apm
|
||||
- context7-microsoft-apm # partial: earlier pass only; the 2026-09-28 re-verification could not reach Context7 (see sources.md)
|
||||
---
|
||||
|
||||
Checked against the installed apm-cli **0.28.0** source (`integration/prompt_integrator.py`, `integration/command_integrator.py`, `integration/base_integrator.py`, `integration/targets.py`, `security/gate.py`) and a live `apm install` of a scratch package targeting `claude` and `copilot` (2026-09-28).
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
## apm-cli-installed-source
|
||||
|
||||
- **URL:** file:///root/.local/pipx/venvs/apm-cli/lib/python3.11/site-packages/apm_cli/
|
||||
- **URL:** https://github.com/microsoft/apm/tree/v0.28.0/src/apm_cli/
|
||||
- **Local copy read:** the installed package at `~/.local/pipx/venvs/apm-cli/lib/python3.11/site-packages/apm_cli/` (pipx, apm-cli 0.28.0), which is what was actually read; the URL above is the matching upstream tag, for readers without that install.
|
||||
- **Description:** Installed apm-cli 0.28.0 package source, which is the version this repo runs. Read as ground truth for the hooks, instructions and prompts docs, including `integration/hook_integrator.py`, `hook_native_formats.py`, `hook_ir.py`, `hook_file_routing.py`, `instruction_integrator.py`, `command_integrator.py`, `prompt_integrator.py`, `targets.py`, `primitives/`, `utils/patterns.py`, `compilation/agents_compiler.py`, `commands/compile/cli.py` and `security/executables.py`. Cross-checked by live `apm install` / `apm compile` runs of a throwaway package (claude and copilot targets) in a scratch dir on 2026-09-28.
|
||||
- **Contributing files:** hooks-primitive-schema.md, instructions-primitive-schema.md, prompt-primitive-schema.md
|
||||
- **Status:** `extracted`
|
||||
@@ -28,6 +29,6 @@
|
||||
- **Contributing files:** hooks-primitive-schema.md, instructions-primitive-schema.md, prompt-primitive-schema.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
Note (2026-09-28): during the verification pass for the hooks, instructions and prompts docs, the Context7 `/microsoft/apm` endpoint returned an invalid-API-key error. Those three files were re-verified against `apm-cli-installed-source` and `apm-docs-llms-full` only. Their `context7-microsoft-apm` key reflects the earlier pass.
|
||||
Note (2026-09-28): during the verification pass for the hooks, instructions and prompts docs, the Context7 `/microsoft/apm` endpoint returned an invalid-API-key error. Those three files were re-verified against `apm-cli-installed-source` and `apm-docs-llms-full` only. Their `context7-microsoft-apm` key reflects the earlier pass, and each of the three carries a note saying so next to the key.
|
||||
|
||||
Note: `releasing.md`'s `--check-clean`/`--check-versions` scope, `apm pack` exit-code semantics, and the `.apm/`-vs-root-flat-dir mutual exclusivity referenced there were additionally cross-checked directly against `apm_cli/bundle/plugin_exporter.py`, `apm_cli/commands/pack.py`, and `apm_cli/marketplace/drift_check.py` in the installed `apm-cli` 0.28.0 package (`/root/.local/pipx/venvs/apm-cli/`), not just Context7 doc snippets — confirmed by a live `apm pack --format plugin` run inside `plugins/bin` that reproduced the documented `[!] Skipping root-level skills/ because .apm/ is present` warning.
|
||||
|
||||
Reference in New Issue
Block a user