diff --git a/CONTEXT.md b/CONTEXT.md index 282603c..fbddf2f 100644 --- a/CONTEXT.md +++ b/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//.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//.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. diff --git a/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md b/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md index 665133b..ede7195 100644 --- a/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md +++ b/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md @@ -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//`. The sidecar and the script directory are gitignored install output; the +`.claude/hooks//`. 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`. diff --git a/docs/adr/0022-skill-metadata-version-is-mandatory.md b/docs/adr/0022-skill-metadata-version-is-mandatory.md index cf69d46..25c50d2 100644 --- a/docs/adr/0022-skill-metadata-version-is-mandatory.md +++ b/docs/adr/0022-skill-metadata-version-is-mandatory.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 diff --git a/docs/adr/0025-skill-audit-and-agent-audit-merge-into-factory-audit.md b/docs/adr/0025-skill-audit-and-agent-audit-merge-into-factory-audit.md index e8b3685..c5a508a 100644 --- a/docs/adr/0025-skill-audit-and-agent-audit-merge-into-factory-audit.md +++ b/docs/adr/0025-skill-audit-and-agent-audit-merge-into-factory-audit.md @@ -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 diff --git a/docs/adr/0029-prompts-are-thin-user-triggered-steering-messages.md b/docs/adr/0029-prompts-are-thin-user-triggered-steering-messages.md index 017a69f..175322b 100644 --- a/docs/adr/0029-prompts-are-thin-user-triggered-steering-messages.md +++ b/docs/adr/0029-prompts-are-thin-user-triggered-steering-messages.md @@ -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 diff --git a/docs/spec/gates.md b/docs/spec/gates.md index b19986e..3ca2499 100644 --- a/docs/spec/gates.md +++ b/docs/spec/gates.md @@ -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. diff --git a/plugins/kyberforge/README.md b/plugins/kyberforge/README.md index c519e7b..2cea2e4 100644 --- a/plugins/kyberforge/README.md +++ b/plugins/kyberforge/README.md @@ -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 diff --git a/plugins/kyberforge/docs/hooks.md b/plugins/kyberforge/docs/hooks.md index 75c3e42..816bc7c 100644 --- a/plugins/kyberforge/docs/hooks.md +++ b/plugins/kyberforge/docs/hooks.md @@ -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 diff --git a/plugins/kyberforge/docs/research/docs/microsoft-apm/hooks-primitive-schema.md b/plugins/kyberforge/docs/research/docs/microsoft-apm/hooks-primitive-schema.md index 2b08703..71aec10 100644 --- a/plugins/kyberforge/docs/research/docs/microsoft-apm/hooks-primitive-schema.md +++ b/plugins/kyberforge/docs/research/docs/microsoft-apm/hooks-primitive-schema.md @@ -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. diff --git a/plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-primitive-schema.md b/plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-primitive-schema.md index 10d24b8..99ea4ac 100644 --- a/plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-primitive-schema.md +++ b/plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-primitive-schema.md @@ -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). diff --git a/plugins/kyberforge/docs/research/docs/microsoft-apm/prompt-primitive-schema.md b/plugins/kyberforge/docs/research/docs/microsoft-apm/prompt-primitive-schema.md index 3708a3c..24a3055 100644 --- a/plugins/kyberforge/docs/research/docs/microsoft-apm/prompt-primitive-schema.md +++ b/plugins/kyberforge/docs/research/docs/microsoft-apm/prompt-primitive-schema.md @@ -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). diff --git a/plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md b/plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md index 4a2cf8a..0c0846a 100644 --- a/plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md +++ b/plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md @@ -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.