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:
2026-09-29 08:00:39 +00:00
parent 4a4b598955
commit 641ebcac0e
12 changed files with 171 additions and 76 deletions

View File

@@ -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 _Avoid_: rule (the Claude-side deployed form under `.claude/rules/`), guideline, standard
**Hook**: **Hook**:
A runtime callback a harness fires inside its own tool loop, authored as one JSON file per concern A runtime callback a harness fires inside its own tool loop, authored as JSON under
under `plugins/<plugin>/.apm/hooks/` in apm's canonical shape — nested entries, PascalCase events, `plugins/<plugin>/.apm/hooks/` — one file or several; kyberforge ships a single `hooks.json` — in
`${PLUGIN_ROOT}` script paths — which apm renders per target. Reach is narrowed in the package's 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 `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". 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) _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 recheck belongs to `factory-audit`, not to `forge`, which routes only to the author
skills and never to an audit. skills and never to an audit.
- "prompt" meant both apm's `.prompt.md` primitive and, loosely, any slash command or a skill — resolved: - "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 a **Prompt** is the `.prompt.md` primitive under the house rule above. apm frames a prompt as a
callable program for an LLM", but on Claude it deploys as a model-invocable command with fewer program ("a prompt is a program for an LLM", its "What is APM?" page), but on Claude it deploys
frontmatter keys than a skill, and Codex receives nothing. A fat prompt is a worse skill on every as a model-invocable command with fewer frontmatter keys than a skill, and Codex receives
harness, so the procedure goes in the skill and the prompt only steers it. nothing. A fat prompt is a worse skill on every harness, so the procedure goes in the skill and
the prompt only steers it.

View File

@@ -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 *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 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 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 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 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 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, 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. 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 - **`startup` matcher only.** `resume`, `clear`, `compact` and `fork` — the other documented
compaction, and a compaction is not an event after which the remote can have moved. `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:` The executable-trust gate is switched on at the same time. Root `apm.yml` gains an `executables:`
block allowing kyberforge's hooks and bin. 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 > Re-measured: ~24–26 s for the same six-behind refresh, warm, on a LAN remote — well inside the
> 380 s above. > 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` **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 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 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 it does not last. At the next session start the hook finds the restored lock behind `main`
> and refreshes again. > 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 **`.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 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 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 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. 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 **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 `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 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`. no hook at all, for the reasons already documented in `plugins/kyberforge/docs/hooks.md`.

View File

@@ -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 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. 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 ## Consequences
27 SKILL.md files gain `metadata.version: "1.0.0"`, and a 28th — `bin/write-docs` — reaches the same 27 SKILL.md files gain `metadata.version: "1.0.0"`, and a 28th — `bin/write-docs` — reaches the same

View File

@@ -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 Its agent row ("a path under `.apm/agents/`… or an agent markdown file") was both wider than the
script and circular. 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.** **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` - `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, 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. 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 ## Considered options
**Keep two skills and rely on the byte-identity contract test alone (rejected).** This is the status **Keep two skills and rely on the byte-identity contract test alone (rejected).** This is the status

View File

@@ -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, 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. 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 This is stricter than apm. apm frames a prompt as a program: its docs state that "a prompt is a
scaffolds one as a numbered-steps workflow (`apm_cli/workflow/discovery.py`). On every harness this program for an LLM" (microsoft.github.io/apm/concepts/what-is-apm/, "Secure by default"; the same
repo targets, a prompt with a full workflow in it is a worse skill: 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 - **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). 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 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 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. 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 - **Copilot / VS Code.** "Prompt files are deprecated for Agent Host sessions and aren't loaded by
migration to agent skills. 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. - **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" 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). - `description` is present and non-empty (FAIL).
- It is 250 characters or fewer (SUGGESTION). - It is 250 characters or fewer (SUGGESTION).
- It has no "Use when" trigger clause (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 - **`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 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. 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 ## Considered options
- **Fat workflow prompts as peers of skills (rejected).** This follows apm's framing. But every - **Fat workflow prompts as peers of skills (rejected).** This follows apm's framing. But every

View File

@@ -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 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**, `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` 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: and `KyberforgeCopilot` styles and a single `.vale.ini` with five glob sections:
`[**/SKILL.md]`, `[**/agents/*.md]`, `[**/*.agent.md]`. `[**/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 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 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 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 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 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 **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 `.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: 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 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 *and* a Kyberforge alert (case 28). Case 28 also checks that every `.vale.ini` section has an
published vale hook, and that every `.vale.ini` section has a probe row. Its Part B drops 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 `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 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 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 **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 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 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 [A 0-file Vale run is NOT RUN](#a-0-file-vale-run-is-not-run)). One `.vale.ini` carrying every
sections removes that constraint. The split stays anyway because the `files:` regexes still have to 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 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 plugin-bundled `factory-audit/scripts/vale-wrap.sh` through `repo: local`; there is no second root
copy. copy.
### The `.vale.ini` globs do no scoping ### The `.vale.ini` globs do no scoping
The `.vale.ini`'s section globs are **path-agnostic** — `[**/SKILL.md]`, `[**/agents/*.md]` and The `.vale.ini`'s section globs are **path-agnostic** — `[**/SKILL.md]`, `[**/agents/*.md]`,
`[**/*.agent.md]` — and constrain filename *shape*, not `[**/*.instructions.md]`, `[**/*.prompt.md]` and `[**/*.agent.md]` — and constrain filename *shape*,
location: Vale's `*` crosses `/`. A `SKILL.md` outside `plugins/` (a project-scope 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. `.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 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: 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. `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 `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 `tests/test-vale-wrap.sh` with the hook's deletion. Its table (`PROBE_TABLE28`) now has eight rows,
pin this location independence — see [One copy, one config](#one-copy-one-config). 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 ### The blind spot: `references/` is unlinted, for two independent reasons
@@ -1051,9 +1063,9 @@ clean.
### Pre-push ### Pre-push
`vale` is still a **pre-push** dependency, but no longer through a hook of its own. `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 `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 `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 `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. pass, and a skip fails the push.

View File

@@ -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) | | 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 | `.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 ## Skills

View File

@@ -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 `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. 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 For Claude, apm merges the event bindings from every `*.json` in that directory into the consuming
bindings into the consuming project's `.claude/settings.json`; see "Deployed shape" below. Scripts a project's `.claude/settings.json`; see "Deployed shape" below. That merge is Claude's rendering, not
hook invokes live in the same directory, alongside the JSON that references them. 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 ## 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` Events: see `plugins/kyberforge/docs/research/docs/microsoft-apm/hooks-primitive-schema.md`,
(verified end-to-end by the hook below). Claude Code's plugin hook set is larger — `SessionEnd`, section "Events: `_HOOK_EVENT_MAP`", for which names apm renames per target and which it passes
`UserPromptSubmit`, `PreCompact` and `SubagentStop` also exist — and this repo's vendored corpus does through unchanged. For Claude, author every event in PascalCase (`SessionStart`, `UserPromptSubmit`,
not enumerate it anywhere: `docs/research/docs/claude-code-plugins/configuration.md:100` describes `PreCompact`, and so on). A camelCase name apm does not map, such as `userPromptSubmit`, draws only
the file as "Event handlers (PreToolUse, PostToolUse, etc.)", and `agent-definition.md:53` covers a non-fatal warning and never fires; an all-lowercase one draws no warning at all and never fires
only the per-agent `hooks` field, not the plugin-level set. Treat the five names above as the ones either.
this repo has verified, not as the schema. Check Claude Code's own hooks documentation before wiring
an event not listed here.
## Referencing a script — use the `.apm/` path ## 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`, `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 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. ADR-0019.
**Claude Code only, and where it looks for the lockfile.** The hook exits 0 at once, silently and **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). 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 **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 `timeout -k 5 60 apm outdated` plus `timeout -k 5 300 apm update`, 370 s counting each 5 s SIGKILL
sum plus a buffer. Set it lower and a slow remote gets the hook SIGKILLed mid-`apm update`, leaving a 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 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 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. 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` **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 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 greps its text. apm prints `1 outdated dependency found` in the singular when exactly one package is

View File

@@ -4,7 +4,7 @@ source_keys:
- apm-cli-installed-source - apm-cli-installed-source
- apm-docs-llms-full - apm-docs-llms-full
- apm-github-repo - 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. 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.

View File

@@ -4,7 +4,7 @@ source_keys:
- apm-cli-installed-source - apm-cli-installed-source
- apm-docs-llms-full - apm-docs-llms-full
- apm-github-repo - 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). 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).

View File

@@ -4,7 +4,7 @@ source_keys:
- apm-cli-installed-source - apm-cli-installed-source
- apm-docs-llms-full - apm-docs-llms-full
- apm-github-repo - 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). 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).

View File

@@ -16,7 +16,8 @@
## apm-cli-installed-source ## 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. - **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 - **Contributing files:** hooks-primitive-schema.md, instructions-primitive-schema.md, prompt-primitive-schema.md
- **Status:** `extracted` - **Status:** `extracted`
@@ -28,6 +29,6 @@
- **Contributing files:** hooks-primitive-schema.md, instructions-primitive-schema.md, prompt-primitive-schema.md - **Contributing files:** hooks-primitive-schema.md, instructions-primitive-schema.md, prompt-primitive-schema.md
- **Status:** `extracted` - **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. 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.