feat(kyberforge): add primitive-author for apm hooks, instructions and prompts
New skill that creates or improves an apm hook, instruction or prompt. Its SKILL.md holds the shared procedure (dispatch on primitive, boundary gate, create-or-improve, factory-audit close); one self-contained reference per primitive carries its gate, checklist and template, drawn from the microsoft-apm research docs and ADR-0029. forge gains a route row sending a hook, instruction or prompt to primitive-author through author-routes.md, and no longer lists hooks as unroutable. factory-audit's description adds the primitive-author boundary now that the target resolves. Fixes #94 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:
52
plugins/kyberforge/.apm/skills/primitive-author/SKILL.md
Normal file
52
plugins/kyberforge/.apm/skills/primitive-author/SKILL.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: primitive-author
|
||||
description: >
|
||||
Use when the user wants an apm hook, instruction or prompt file created, or
|
||||
audit findings or feedback applied to an existing one.
|
||||
Not skills -> skill-author. Not agents -> agent-author.
|
||||
allowed-tools: Bash Read Write Edit
|
||||
metadata:
|
||||
version: "0.1.0"
|
||||
category: factory
|
||||
source_keys:
|
||||
- apm-cli-installed-source
|
||||
- apm-docs-llms-full
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `apm compile --validate` is not a gate. apm turns every instruction and prompt problem into a warning and exits 0, and `apm install` never validates at all — `/factory-audit` is the only check that fails.
|
||||
- Never draft with the real suffix outside `.apm/<type>/`. apm's local discovery globs `**/*.instructions.md` across the whole tree, so a draft or template named that way anywhere in the repo compiles into `AGENTS.md`. The templates carry a trailing `.template` for this reason; drop it only on the final path.
|
||||
- Never hand-write `.claude/settings.json`, even to test a hook. apm owns that file (ADR-0019), overwrites it outright when it is malformed, and `apm audit --ci` fails on anything it would not have written.
|
||||
|
||||
## Step 1 — Dispatch
|
||||
|
||||
| Target or intent | Primitive | Reference |
|
||||
|---|---|---|
|
||||
| A hook — `.apm/hooks/<name>.json`, or "run X whenever Y happens" | hook | `references/hook.md` |
|
||||
| An instruction — `.apm/instructions/<name>.instructions.md`, or a rule for files matching a pattern | instruction | `references/instruction.md` |
|
||||
| A prompt — `.apm/prompts/<name>.prompt.md`, or a reusable message the user types to kick off work | prompt | `references/prompt.md` |
|
||||
| A skill or an agent | — | stop: route to `skill-author` or `agent-author` |
|
||||
|
||||
Read only the reference matching the resolved primitive — each is self-contained. If the target sits inside a git worktree, capture `rtk git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
|
||||
## Step 2 — Boundary gate
|
||||
|
||||
Run the reference's **Gate** section before writing anything. A failed gate stops this skill: name the owner it points to — `skill-author` for procedure, `agentsmd-author` for a repo-only rule, `apm-workflow` for reach or `targets:` — and hand over. Never bend the artifact to pass the gate.
|
||||
|
||||
## Step 3 — Create or improve
|
||||
|
||||
| Condition | Action |
|
||||
|---|---|
|
||||
| No file at the target path | Create: copy the reference's template from `assets/templates/`, drop `.template`, fill every `FILL IN`, and apply the reference's checklist |
|
||||
| File exists, at least one signal | Improve: read the whole file, then apply each signal against the reference's checklist |
|
||||
| File exists, no signal | Stop and ask whether the user meant a new file or has feedback to apply |
|
||||
|
||||
Signals: grill output, `/factory-audit` findings, inline feedback, session context describing what went wrong. Group findings by root cause and fix the cause once.
|
||||
|
||||
## Step 4 — Validate and close
|
||||
|
||||
1. Run `/factory-audit` on the file, inline in this context; resolve every FAIL before reporting done.
|
||||
2. Run `rtk apm install --dry-run` from the repo root and read what each target will receive. On a feature branch, discard `apm.lock.yaml` churn afterwards (`rtk git checkout -- apm.lock.yaml`).
|
||||
3. Bump the owning package's `apm.yml` `version:` — minor for a new primitive, patch for a fix — unless this branch already bumped it for unreleased work. A primitive has no version of its own.
|
||||
4. **Commit verification.** Inside a git worktree, once the audit is clean, run `rtk git add` and `rtk git commit`, then re-run `rtk git log --oneline -1` and confirm the hash changed from Step 1's. Staged-but-uncommitted work is silently lost if the tree is cleaned up. Outside a worktree, report done on a clean audit and name that as the reason.
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hooks": {
|
||||
"FILL_IN_PascalCaseEvent": [
|
||||
{
|
||||
"matcher": "FILL_IN_matcher",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${PLUGIN_ROOT}/.apm/hooks/FILL_IN_script.sh",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
description: "FILL IN: one sentence on what this rule governs"
|
||||
applyTo: "FILL IN: glob, e.g. **/*.{ts,tsx}"
|
||||
---
|
||||
|
||||
FILL IN: the rule, as direct second-person guidance. Put any rationale Claude needs here — the
|
||||
description above never reaches Claude.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: "FILL IN: one user-facing action naming the skills it steers"
|
||||
input:
|
||||
- FILL_IN_name: "FILL IN: what the user supplies"
|
||||
---
|
||||
|
||||
FILL IN: the message the user would otherwise type, steering existing skills or agents by name.
|
||||
Use ${input:FILL_IN_name} where the value belongs.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
source_keys:
|
||||
- apm-cli-installed-source
|
||||
- apm-docs-llms-full
|
||||
---
|
||||
|
||||
# Authoring an apm hook
|
||||
|
||||
Reached from `SKILL.md` Step 1 for a hook. Run the Gate, then write against the shape and the
|
||||
checklist, then return to `SKILL.md` Step 3.
|
||||
|
||||
## Gate
|
||||
|
||||
A hook is a runtime callback the harness fires inside its own tool loop — "this must always
|
||||
happen at this event", enforced deterministically rather than left to the model. apm's own
|
||||
guidance is to reach for a skill, instruction or prompt first, and to treat hooks as opt-in
|
||||
surface: they ship to a strict subset of harnesses and are silently skipped everywhere else.
|
||||
|
||||
- **Procedure, know-how, or anything the model should decide to do** → a skill. Stop and hand to
|
||||
`skill-author`.
|
||||
- **The hook must reach only some harnesses** → reach is set by the package `apm.yml` `targets:`,
|
||||
never by the hook file. Stop and hand to `apm-workflow`. `targets:` is package-wide, so a
|
||||
harness-specific hook in a multi-target package means either a separate package or accepting that
|
||||
the other targets receive it too.
|
||||
- **A runtime callback** → continue.
|
||||
|
||||
## Shape
|
||||
|
||||
One JSON file per concern at `.apm/hooks/<name>.json`, with a plain name. Copy
|
||||
`assets/templates/hook.json.template`. Write the canonical shape apm documents and renders per
|
||||
target:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{"type": "command", "command": "${PLUGIN_ROOT}/.apm/hooks/check.sh", "timeout": 10}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`${PLUGIN_ROOT}`** is the target-neutral token; apm rewrites it per target
|
||||
(`"${CLAUDE_PROJECT_DIR}/.claude/hooks/<pkg>/…"` on Claude, repo-relative elsewhere).
|
||||
`${CLAUDE_PLUGIN_ROOT}` also works but ties the source to one harness's name.
|
||||
- **Claude is the verified target.** apm 0.28.0 passes this nested shape to Copilot without
|
||||
reshaping it, and whether Copilot CLI runs nested entries or honours `matcher` is unverified. That
|
||||
gap is apm's to close — do not work around it with a second, Copilot-flat file.
|
||||
|
||||
## Checklist
|
||||
|
||||
Must:
|
||||
|
||||
1. The file sits directly in `.apm/hooks/`, is not a symlink or hardlink, and parses as a JSON
|
||||
object. apm skips invalid JSON silently.
|
||||
2. Every event value is a list of objects, and every nested `hooks` is a list of objects. Anything
|
||||
else fails the Copilot install outright.
|
||||
3. Event names are PascalCase (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart`,
|
||||
`Stop`, …). An all-lowercase name (`stop`) never warns and never fires; a camelCase name outside
|
||||
apm's rename map (`userPromptSubmit`) deploys verbatim to Claude and never fires.
|
||||
4. The script is referenced as `${PLUGIN_ROOT}/…` (package root) or `./…` (hook directory),
|
||||
exists inside the package, and is executable. No absolute path, and no `$` or backtick in the
|
||||
path itself. A missing script is only a warning at install time.
|
||||
5. No `hooks-<target>` or `*-<target>-hooks` filename. That routing is deprecated; reach belongs to
|
||||
`targets:` (see Gate).
|
||||
|
||||
Should:
|
||||
|
||||
6. Every handler sets `"type": "command"` and an explicit `timeout` in seconds — apm passes `type`
|
||||
through but never supplies it.
|
||||
7. Set `matcher` explicitly on tool events and on `SessionStart` (`startup`, `resume`, …). Omitted,
|
||||
Claude receives `"*"`.
|
||||
8. Do not author Copilot's flat `bash` / `powershell` / `timeoutSec` keys in a Claude-shaped file;
|
||||
they render onto Claude as stray keys.
|
||||
9. Quote a script path that may contain spaces: `"${PLUGIN_ROOT}/scripts/my hook.sh"`.
|
||||
10. Keep helper files in the hook directory non-JSON. Copilot's loader rejects any bundled `.json`
|
||||
without a `hooks` key.
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
source_keys:
|
||||
- apm-cli-installed-source
|
||||
- apm-docs-llms-full
|
||||
---
|
||||
|
||||
# Authoring an apm instruction
|
||||
|
||||
Reached from `SKILL.md` Step 1 for an instruction. Run the Gate, then write against the checklist,
|
||||
then return to `SKILL.md` Step 3.
|
||||
|
||||
## Gate
|
||||
|
||||
An instruction is a scoped rule: it applies when the agent touches files matching its `applyTo`
|
||||
glob. On Claude it deploys to `.claude/rules/<stem>.md` with `applyTo` renamed to `paths:`.
|
||||
|
||||
- **A rule for this repo alone** → it belongs in the repo's AGENTS.md, which is the single
|
||||
always-on source. Stop and hand to `agentsmd-author`.
|
||||
- **No file pattern fits** → an instruction without `applyTo` is always-on in every session of
|
||||
every repo that installs this package, and `apm compile` folds it into the global sections of
|
||||
`AGENTS.md` and `CLAUDE.md`. Say exactly that to the user and continue only on an explicit yes.
|
||||
Legitimate when a package deliberately ships guidance to its consumers; never a default.
|
||||
- **Procedure the agent follows step by step** → a skill. Stop and hand to `skill-author`.
|
||||
- **A rule scoped to a file pattern** → continue.
|
||||
|
||||
## Checklist
|
||||
|
||||
Copy `assets/templates/name.instructions.md.template` and drop `.template` only on the final path.
|
||||
|
||||
Must:
|
||||
|
||||
1. The path is `.apm/instructions/<stem>.instructions.md`, directly in that directory, not a
|
||||
symlink or hardlink.
|
||||
2. `description` is a non-empty string. apm only warns when it is missing.
|
||||
3. The body is non-empty. apm deploys an empty rule silently.
|
||||
4. `applyTo` is a non-empty glob or comma-separated list — top-level commas only as separators,
|
||||
alternation inside `{}` (`"**/*.{ts,tsx}"`) — or absent after the Gate's explicit yes.
|
||||
5. The stem is unique across the package and its dependencies: a `.claude/rules/<stem>.md`
|
||||
collision is silently overwritten.
|
||||
|
||||
Should:
|
||||
|
||||
6. Write `applyTo` as a scalar string, not a YAML list. Copilot receives the source verbatim, and
|
||||
its handling of a list is unverified.
|
||||
7. Keep frontmatter to `description` and `applyTo`, plus optional `author` and `version`. No target
|
||||
consumes other keys, and Claude drops them.
|
||||
8. Put any rationale Claude needs in the body. `description` never reaches Claude — it survives only
|
||||
for Copilot and as Cursor's index text.
|
||||
9. Keep relative markdown links resolvable from the source file.
|
||||
10. Check the glob against the tree: one that matches nothing never fires, and one broader than the
|
||||
rule's real scope spends context on every file it touches.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
source_keys:
|
||||
- apm-cli-installed-source
|
||||
- apm-docs-llms-full
|
||||
- adr-0029-prompt-house-rule
|
||||
---
|
||||
|
||||
# Authoring an apm prompt
|
||||
|
||||
Reached from `SKILL.md` Step 1 for a prompt. Run the Gate, then write against the description
|
||||
contract and the checklist, then return to `SKILL.md` Step 3.
|
||||
|
||||
## Gate
|
||||
|
||||
This repo holds a prompt to ADR-0029, which is stricter than apm: apm calls a prompt "a callable
|
||||
program", but on Claude it deploys as a command that is a skill in every respect except that it
|
||||
keeps fewer frontmatter keys, apm drops `disable-model-invocation` so it can never be made
|
||||
user-only, and Codex receives no prompts at all. A prompt that carries procedure is therefore a
|
||||
worse skill on every harness.
|
||||
|
||||
- **Reusable know-how, steps, gotchas, bundled files, or anything the model should find on its
|
||||
own** → a skill. Stop and hand to `skill-author`; if a short steering message is still wanted
|
||||
afterwards, come back and write it against the new skill.
|
||||
- **A single-intent message the user would otherwise type repeatedly, steering existing skills or
|
||||
agents by name** → continue. Confirm each skill or agent it names exists and is not
|
||||
`disable-model-invocation: true`, which the model cannot invoke.
|
||||
|
||||
## Description contract
|
||||
|
||||
One plain, user-facing sentence stating the action and naming the skills it steers — "Review the
|
||||
current PR with `gitea-prs` and `factory-audit`, then summarise the findings." No "Use when"
|
||||
trigger clause and no `Not X -> Y` boundary: on Claude the description is model-visible, and a
|
||||
trigger clause invites the router to pick the wrapper over the skills it wraps. 250 characters at
|
||||
most.
|
||||
|
||||
## Checklist
|
||||
|
||||
Copy `assets/templates/name.prompt.md.template` and drop `.template` only on the final path.
|
||||
|
||||
Must:
|
||||
|
||||
1. The path is `.apm/prompts/<name>.prompt.md`, directly in that directory, not a symlink. `<name>`
|
||||
is a safe path segment and unique across `.apm/prompts/` and the package root; it becomes the
|
||||
Copilot filename and the Claude `/command` name.
|
||||
2. `description` is present and non-empty, per the contract above.
|
||||
3. Every `input:` name matches `^[A-Za-z][\w-]{0,63}$`, written in the object form
|
||||
`- pr_number: "The PR to review"`. Never copy apm's published `- name: pr_number` /
|
||||
`description: …` example: apm reads the map's keys, so it produces the arguments `name` and
|
||||
`description`.
|
||||
4. Every `${input:x}` in the body is declared in `input:`, and every declared name is used. Without
|
||||
`input:`, no `${input:…}` may appear — it would reach Claude unrewritten.
|
||||
5. Frontmatter keys stay within `description`, `allowed-tools`, `model`, `argument-hint` and
|
||||
`input`. Claude drops everything else with only a warning.
|
||||
|
||||
Should:
|
||||
|
||||
6. Spell keys in kebab-case — `allowed-tools`, `argument-hint` — not the camelCase aliases.
|
||||
7. Omit `argument-hint` when `input:` is set; apm synthesises `<a> <b>` from the input names.
|
||||
8. Keep one intent per prompt, and write the body as second-person instructions.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Sources
|
||||
|
||||
## apm-cli-installed-source
|
||||
|
||||
- **URL:** file:///root/.local/pipx/venvs/apm-cli/lib/python3.11/site-packages/apm_cli/
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md
|
||||
- **Description:** Installed apm-cli 0.28.0 source — ground truth for what apm deploys from a hook, instruction or prompt file and what it silently skips or only warns on; every Must/Should checklist item traces to the research docs' Authoring checklists, and the `**/*.instructions.md` local-discovery glob behind the template-suffix Gotcha is `primitives/discovery.py` `LOCAL_PRIMITIVE_PATTERNS`
|
||||
- **Contributing files:** SKILL.md, references/hook.md, references/instruction.md, references/prompt.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## apm-docs-llms-full
|
||||
|
||||
- **URL:** https://microsoft.github.io/apm/llms-full.txt
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md
|
||||
- **Description:** Published apm docs bundle — the "Hooks and commands" guide's canonical hook shape, `${PLUGIN_ROOT}`, reach via `targets:` rather than deprecated filename routing, and "reach for a skill, instruction, or prompt first"; the "Author a prompt" guide's one-intent rule
|
||||
- **Contributing files:** SKILL.md, references/hook.md, references/instruction.md, references/prompt.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## adr-0029-prompt-house-rule
|
||||
|
||||
- **URL:** docs/adr/0029-prompts-are-thin-user-triggered-steering-messages.md
|
||||
- **Research doc:** none
|
||||
- **Basis:** docs/adr/0029-prompts-are-thin-user-triggered-steering-messages.md
|
||||
- **Description:** The repo's prompt house rule — a prompt is a single-intent, user-triggered steering message with no procedure — and its description contract: one user-facing sentence naming the steered skills, no trigger or boundary clause
|
||||
- **Contributing files:** references/prompt.md
|
||||
- **Status:** `extracted`
|
||||
Reference in New Issue
Block a user