14 Commits
Author SHA1 Message Date
Defame1297 17d67fbfa9 Merge pull request 'fix(pre-commit): keep key order in pretty-format-json so apm-owned JSON survives' (#146) from fix/pretty-json-no-sort-keys-102 into main
Reviewed-on: #146
2026-09-30 16:47:29 +00:00
Defame1297andClaude Code 18fbdbc8e4 refactor(pre-commit): drop the tool-owned round-trip test for #102
The apm-audit-ci pre-push hook already fails when pretty-format-json sorts
apm-owned JSON, so a dedicated test only improved the diagnosis while adding
~100 lines of bash and a pre-commit cache dependency. Remove the test and the
comment, gates.md and LESSONS.md text that pointed at it; the --no-sort-keys
fix itself is unchanged.

Refs: #102

Co-Authored-By: Claude Code <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
2026-09-30 16:37:25 +00:00
Defame1297andClaude Code c2c56ff948 fix(pre-commit): keep key order in pretty-format-json so apm-owned JSON survives
pretty-format-json sorts object keys by default, but apm emits insertion
order and `apm audit --ci` diffs its output byte-for-byte. A file in the
formatter's scope therefore drifts on every commit with an empty git diff.

Pass --no-sort-keys so `.claude/settings.json` and `.claude/apm-hooks.json`
round-trip untouched and drop them from the exclude. marketplace.json stays
excluded: it carries literal em dashes the formatter re-escapes to —.

tests/test-pretty-json-tool-owned.sh runs the repo's real autofix hooks over
copies of the four tracked apm-owned files and fails if any is rewritten, so
losing the flag fails a test instead of surfacing as drift.

Refs: #102

Co-Authored-By: Claude Code <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
2026-09-30 16:02:21 +00:00
Defame1297 3ff0741857 Merge pull request 'fix(kyberforge): trim forge description and body to ADR-0020 budgets' (#145) from fix/forge-adr-0020-budgets into main
Reviewed-on: #145
2026-09-30 15:27:41 +00:00
Defame1297andClaude Code 8c583b5fd5 chore(kyberforge): sync executables allow key and marketplace to 2.0.2
The kyberforge version bump needs the matching executables.allow key in
the root apm.yml (ADR-0019) and a regenerated checked-in marketplace.json,
or the pre-push gates check-executables-allow-sync and apm pack
--check-clean fail.

Refs: #143

Co-Authored-By: Claude Code <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
2026-09-30 07:10:36 +00:00
Defame1297andClaude Code cacfa1b374 fix(kyberforge): trim forge description and body to ADR-0020 budgets
factory-audit flagged forge's description and body as over the ADR-0020
targets (250 chars / 600 words). The description now uses three boundary
clauses and the body is 595 words. The context: fork vs /fork gotcha moved
to references/author-routes.md, the only place the fork-or-inline choice
is made. No routing row or behaviour changed.

Fixes: #143

Co-Authored-By: Claude Code <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
2026-09-30 06:58:40 +00:00
Defame1297 f30fbacf14 Merge pull request 'chore: migrate git host from git.dev.rkdr.net to git.rkdr.net' (#142) from chore/git-host-migration into main
Reviewed-on: #142
2026-09-25 14:23:16 +00:00
Defame1297 f22836ff7e chore: merge main (bats build/ exclusion fix) into chore/git-host-migration 2026-09-25 14:15:22 +00:00
Defame1297 4357da5b4d Merge pull request 'fix(tests): exclude build/ from bats test discovery' (#141) from fix/bats-repo-root-resolution into main
Reviewed-on: #141
2026-09-25 14:13:20 +00:00
Defame1297 b6a5915520 fix(tests): exclude build/ from bats test discovery
Why
tests/run-bats.sh's discovery walk already excludes apm_modules/ and
.claude/skills/ because those hold apm-installed copies of the same
*.bats files one directory level shallower than their plugins/*/.apm/
source, which overshoots the hardcoded six-levels-up REPO_ROOT walk
each test's setup() does and fails to find the bats-support helper.
build/ was missing the same exclusion: apm pack stages an identical
copy under build/<package>-<version>/ before archiving, hitting the
exact same failure mode from a different apm subcommand. A stray
local `apm pack` run leaves that directory on disk (gitignored,
regenerable) and silently doubles the suite (846 tests instead of
423) with 423 of them failing.

Implementation Notes
Added `-not -path "*/build/*"` to the find walk and the matching
git ls-files grep exclusion, mirroring the existing apm_modules/ and
.claude/skills/ entries. Extended tests/test-run-bats.sh with a case
following the same pattern as the existing exclusion-bug fixtures.

Impact
Unblocks the run-tests pre-commit/pre-push hook for any checkout that
has ever run a bare `apm pack` locally.
2026-09-25 13:45:25 +00:00
Defame1297 025ad4a5af chore: migrate git host from git.dev.rkdr.net to git.rkdr.net
Why
The repo's git host moved from git.dev.rkdr.net to git.rkdr.net. The
`origin` remote was already repointed; this commit brings every
in-repo reference in line so cloning, submodule init, and apm install
all resolve against the new host.

Implementation Notes
- .gitmodules: docs/wiki submodule URL repointed (tests/* submodules
  stay on github.com, untouched).
- Root apm.yml: 7 dependency entries and marketplace.owner.url
  repointed; executables.allow key updated to kyberforge#2.0.1 to
  match kyberforge's bump below (scripts/check-executables-allow-sync.sh
  enforces this pairing).
- Each plugin's apm.yml (bin, core, git, gitea, kyberforge, lint,
  onedev): author.url/homepage/repository repointed. Per this repo's
  apm versioning policy, these fields compile verbatim into
  plugin.json, so each package took a patch version bump alongside
  the URL change.
- Root apm.yml version and marketplace.version bumped 0.5.0 -> 0.5.1
  to match (a marketplace-block field and every listed package's
  version moved).
- apm.lock.yaml regenerated via `apm install`; .claude-plugin/marketplace.json
  regenerated via `apm pack --marketplace=claude` so compiled output
  stays in sync with the manifests.

Impact
docs/adr/0015, 0017, and 0018 intentionally keep the old host in their
issue links and examples — they are historical decision records, not
live config. Verified clean: apm pack --check-clean, apm audit --ci,
check-executables-allow-sync.sh, and pre-commit --all-files all pass.
2026-09-25 13:15:25 +00:00
Defame1297 d654dca056 Merge pull request 'fix(gates): check body-level routing targets, not just descriptions' (#140) from fix/124-body-level-routing-targets into main
Reviewed-on: https://git.dev.rkdr.net/Defame1297/holocron/pulls/140
Reviewed-by: Defame1297 <[email protected]>
2026-09-22 15:47:18 +00:00
Defame1297andClaude Sonnet 5 c5f754d3ad fix(gates): check body-level routing targets, not just descriptions
The ADR-0020 boundary resolver (boundary_targets()/unresolved_targets())
only ever read a SKILL.md's description. A target named in the BODY -- a
dispatch table row, a "run X" step, both routine in a 900-word procedure
-- was checked by nothing. Two real instances shipped before either was
caught by reading rather than by a gate: bin/write-docs routed twice to a
deleted `to-prd` skill, and bin/triage told an agent to run a nonexistent
`/setup-matt-pocock-skills` (both fixed in 03abcff; that fix was the
symptom, this gate is the actual ask per #124).

Added a separate, narrower extractor -- body_targets() /
unresolved_body_targets() in the shared lib-boundary-resolver.sh -- rather
than reusing the description resolver at wider scope. The description
gate's sentence-level heuristics (BOUNDARY_MARKER, the follower test,
in-sentence corroboration) are tuned for a one-to-three-sentence routing
clause and misfire on dispatch-table/procedure prose in both directions,
so the body gate reads only explicit route notation (`/name`,
backticked-or-slash-prefixed `-> name` / `-> name`), already the
description gate's own unconditionally-blocking tier.

Three guards were added after running the extractor over the real
39-skill corpus and reading every hit rather than assuming the design was
correct:

- a target must be hyphenated, even in notation -- single-word citations
  like `/fork` (forge, citing Claude Code's own /fork command) and
  `/name` (skill-author, a placeholder) are not routes.
- a bare hyphenated word after any arrow is not notation -- only
  ARROW_MARKED (backticked/slash-prefixed) is used, not NOTATION_ARROW's
  bare form, so ordinary process-chain prose ("prop -> new ref ->
  re-render", caveman) is not read as a route.
- a name immediately preceded by `<` is a closing tag
  (`</what-to-do>`, grill-with-docs), not /name notation.

Wired into both consumers that must agree by contract: scripts/
skill-size-check.sh (the pre-commit hook) and factory-audit's
lib-checks-skill.sh (the audit). Verified identical findings across both
over the whole corpus.

tests/test-adr0020-targets.sh gains a dedicated section pinning the two
live true positives and all three guards. docs/spec/gates.md and
ADR-0020 get a matching amendment.

Fixes: #124
ADR: 0020

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
2026-09-22 15:17:09 +00:00
Defame1297 3ea057794c Merge pull request 'feat(kyberforge): make Research doc name one Research registry' (#139) from feat/121-research-doc-grammar into main
Reviewed-on: https://git.dev.rkdr.net/Defame1297/holocron/pulls/139
Reviewed-by: Defame1297 <[email protected]>
2026-09-21 19:52:19 +00:00
29 changed files with 1140 additions and 757 deletions

No files matched your search

+9 -9
View File
@@ -1,59 +1,59 @@
{
"name": "holocron",
"description": "AI development skills for Claude Code, and for GitHub Copilot through apm — factory, design, implement, review, and cross-cutting workflows.",
"version": "0.5.0",
"version": "0.5.1",
"owner": {
"name": "Defame1297",
"email": "[email protected]",
"url": "https://git.dev.rkdr.net/Defame1297/"
"url": "https://git.rkdr.net/Defame1297/"
},
"plugins": [
{
"name": "kyberforge",
"description": "Skills and agents for creating, maintaining, and managing an apm plugin marketplace for Claude Code and GitHub Copilot.",
"version": "2.0.0",
"version": "2.0.2",
"category": "Developer Tools",
"source": "./plugins/kyberforge"
},
{
"name": "bin",
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
"version": "1.1.8",
"version": "1.1.9",
"category": "Utilities",
"source": "./plugins/bin"
},
{
"name": "git",
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
"version": "1.3.8",
"version": "1.3.9",
"category": "Version Control",
"source": "./plugins/git"
},
{
"name": "gitea",
"description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.",
"version": "1.3.9",
"version": "1.3.10",
"category": "Version Control",
"source": "./plugins/gitea"
},
{
"name": "onedev",
"description": "Skills and agents for working with a OneDev forge through the TOD CLI — the forge's own objects, as distinct from the local git clone.",
"version": "0.1.0",
"version": "0.1.1",
"category": "Version Control",
"source": "./plugins/onedev"
},
{
"name": "core",
"description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.",
"version": "1.1.3",
"version": "1.1.4",
"category": "Productivity",
"source": "./plugins/core"
},
{
"name": "lint",
"description": "Skills and agents for configuring and running linters.",
"version": "1.1.8",
"version": "1.1.9",
"category": "Developer Tools",
"source": "./plugins/lint"
}
+1 -1
View File
@@ -12,4 +12,4 @@
ignore = dirty
[submodule "docs/wiki"]
path = docs/wiki
url = git@git.dev.rkdr.net:Defame1297/holocron.wiki.git
url = [email protected]:Defame1297/holocron.wiki.git
+17 -34
View File
@@ -27,41 +27,24 @@ repos:
stages: ['pre-commit']
- id: pretty-format-json
stages: ['pre-commit']
args: [--autofix]
# Every generated manifest lives at a KNOWN path, so every alternative is
# root-anchored and spells that path out. This was five `(^|/)`
# any-depth alternatives plus one `^` root-only one -- a mixture with no
# rationale, under which a fixture or vendored tree containing
# `.../.claude-plugin/marketplace.json` would have been silently excluded
# from formatting while an equivalent
# `.../.agents/plugins/marketplace.json` would not. Only the one root
# marketplace manifest matches now; anything else is hand-authored and
# gets formatted. The twelve per-plugin `plugin.json` alternatives were
# dropped with the plugin manifests themselves when native
# `claude plugin install` support was removed (ADR-0024) -- apm probes
# `apm.yml` and never reached them. The `.agents/plugins/` and
# `.github/plugin/` marketplace mirrors went the same way, and their
# alternations went with them: `check-useless-excludes` fails on a
# pattern that matches no file.
args: [--autofix, --no-sort-keys]
# `--no-sort-keys` is load-bearing. apm OWNS `.claude/settings.json` and its
# `.claude/apm-hooks.json` sidecar (ADR-0018, ADR-0019), and
# `apm audit --ci` replays the install and diffs the result byte-for-byte.
# apm emits insertion order (`matcher` before `hooks`); the formatter's
# default sorts keys, rewrites that into a form apm would never produce,
# and the `apm-audit-ci` pre-push hook then reports drift on a file with
# no git diff (#102, first hit at 2e395a4). Keeping insertion order means
# those two files need no exclude. Dropping the flag is caught at pre-push
# by `apm-audit-ci` as drift on `.claude/settings.json`.
#
# `.claude/settings.json` and its `.claude/apm-hooks.json` ownership
# sidecar are the last two alternations, and they are the only ones
# here for a reason other than "generated manifest":
# apm OWNS that file (ADR-0018, ADR-0019), and
# `apm audit --ci` replays the install into a scratch tree and diffs
# the result byte-for-byte. `pretty-format-json` sorts object keys
# unless `--no-sort-keys` is passed, while apm's hook integrator emits
# insertion order (`matcher` before `hooks`, `type` before `command`).
# Formatting the file therefore rewrites apm's output into a form apm
# would never produce, and the `apm-audit-ci` pre-push hook reports it
# as permanent drift on a file with no git diff -- exactly what
# happened when the SessionStart hook first landed in 2e395a4.
# Re-running `apm install` fixes the file; leaving it in scope here
# would re-break it on the very commit that carries the fix. The
# sidecar is committed so a fresh clone's install can claim the
# settings entry instead of duplicating it (ADR-0019, 2026-09-16
# correction), and it is apm output under the same byte-for-byte replay.
exclude: '^(\.claude-plugin/marketplace\.json|\.claude/(settings|apm-hooks)\.json)$'
# `.claude-plugin/marketplace.json` is the one remaining exclude. It
# round-trips except for non-ASCII: it carries literal em dashes and the
# formatter re-escapes them to `\u2014` (`--no-ensure-ascii` would fix that,
# but it changes the output for every JSON file). Root-anchored because it
# is one known path; `check-useless-excludes` fails on a pattern that
# matches no file.
exclude: '^\.claude-plugin/marketplace\.json$'
- id: check-yaml
stages: ['pre-commit']
- id: trailing-whitespace
+1 -1
View File
@@ -128,7 +128,7 @@ Widening a description-opener rule to also catch mid-sentence text looked like a
## 2026-08-14 — A formatter in the commit path manufactures drift on a file with a clean git diff
`apm audit --ci` failed on `.claude/settings.json` with an empty `git diff` — `pretty-format-json --autofix` silently re-sorts JSON keys, and this generated file was missing from its exclude list, so every commit re-sorted apm's insertion-ordered output before apm compared against it. Separately, a defect introduced 3 hours earlier on the same branch was first mis-described as "pre-existing," an unverified claim about history. Fix: add tool-owned paths to every autofixing hook's exclude the moment ownership is declared, and verify "pre-existing" claims with `git log -S` or `git branch --contains` before writing them down.
`apm audit --ci` failed on `.claude/settings.json` with an empty `git diff` — `pretty-format-json --autofix` silently re-sorts JSON keys, and this generated file was missing from its exclude list, so every commit re-sorted apm's insertion-ordered output before apm compared against it. Separately, a defect introduced 3 hours earlier on the same branch was first mis-described as "pre-existing," an unverified claim about history. Fix: add tool-owned paths to every autofixing hook's exclude the moment ownership is declared (for JSON, superseded by #102: `--no-sort-keys` makes the exclude unnecessary), and verify "pre-existing" claims with `git log -S` or `git branch --contains` before writing them down.
## 2026-08-16 — A rule reversed inside a retrofit leaves no trace unless someone writes it down (historical)
+627 -627
View File
File diff suppressed because it is too large. Load diff
+11 -11
View File
@@ -1,5 +1,5 @@
name: holocron
version: 0.5.0
version: 0.5.1
description: AI development skills for Claude Code, and for GitHub Copilot through apm — factory, design, implement, review, and cross-cutting workflows.
license: MIT
@@ -16,17 +16,17 @@ targets:
- claude
dependencies:
apm:
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: [email protected]:Defame1297/holocron.git
path: plugins/bin
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: [email protected]:Defame1297/holocron.git
path: plugins/core
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: [email protected]:Defame1297/holocron.git
path: plugins/git
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: [email protected]:Defame1297/holocron.git
path: plugins/gitea
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: [email protected]:Defame1297/holocron.git
path: plugins/kyberforge
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: [email protected]:Defame1297/holocron.git
path: plugins/lint
# TOD's skills arrive transitively through this wrapper rather than as a
# direct entry, so the marketplace and this repo consume onedev by the same
@@ -38,7 +38,7 @@ dependencies:
# `apm install` fails, which includes the copy kyberforge's SessionStart
# hook runs on launch. Accepted deliberately: this branch is merging
# immediately.
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: [email protected]:Defame1297/holocron.git
path: plugins/onedev
mcp: []
@@ -61,7 +61,7 @@ dependencies:
# an apm mechanic.
executables:
allow:
kyberforge#2.0.0:
kyberforge#2.0.2:
hooks: true
bin: true
@@ -71,11 +71,11 @@ marketplace:
# top-level apm.yml description:/version: above are NOT inherited into the
# compiled output despite being used elsewhere (e.g. by `apm audit`).
description: AI development skills for Claude Code, and for GitHub Copilot through apm — factory, design, implement, review, and cross-cutting workflows.
version: 0.5.0
version: 0.5.1
owner:
name: Defame1297
email: [email protected]
url: https://git.dev.rkdr.net/Defame1297/
url: https://git.rkdr.net/Defame1297/
# Default tag pattern used to resolve version ranges for each package.
build:
@@ -414,6 +414,56 @@ and rises to a blocking ERROR the moment a resolving sibling joins it. The reaso
the point of enforcement in `_add()`'s docstring in `scripts/skill-size-check.sh` and its two
mirrored copies, and the verdict table in `docs/spec/gates.md` states the corrected shape.
## Amendment (2026-09-22): body-level routing targets are resolved too
The Decision section's routing-target resolver (`boundary_targets()` / `unresolved_targets()`) reads
the **description** only. A target named in the **body** — a dispatch table row, a "run X" step, both
routine in a 900-word procedure — was checked by nothing. Two real instances shipped before either
was caught: `bin/write-docs` routed twice to a deleted `to-prd` skill, and `bin/triage` told an agent
to run a nonexistent `/setup-matt-pocock-skills`. Both were found by reading, not by a gate, during
the #99 retrofit and its follow-up audit; both were fixed in `03abcff`. **The fix this amendment
records is the gate, not those two edits** (issue #124).
The body gate is a **separate, narrower** extractor (`body_targets()` /
`unresolved_body_targets()`), not the description resolver reused at wider scope. The description
resolver's sentence-level heuristics — `BOUNDARY_MARKER`, the follower test, in-sentence
corroboration — are tuned for a one-to-three-sentence routing clause and misfire on dispatch-table
and procedure prose in both directions: under-firing on a table row that carries no "do not" /
"instead", over-firing on a procedure step naming a file, a CLI verb or a config key exactly the way
a route names a skill. Retuning those heuristics for the body genre was considered and rejected as
the harder half of the problem, with a materially worse cost of getting it wrong (a body is loaded
on every invocation, so a false-positive-prone body gate is felt far more often than a
false-positive-prone description gate).
So the body gate reads **only** explicit route notation — `/name` and backticked-or-slash-prefixed
`-> name` / `→ name` — already the description gate's own unconditionally-blocking tier, and nothing
softer: no SUGGESTION tier, no bare-word forms, no corroboration. Two further restrictions, both
earned by a real corpus false positive rather than assumed up front:
- **the target must be hyphenated**, even in notation. `` `/fork` `` (`forge/SKILL.md`, citing
Claude Code's own `/fork` subagent command) and `` `/name` `` (`skill-author/SKILL.md`, a
placeholder for the skill's own name) are real corpus citations of a tool or a placeholder, not
routes, and both hard-FAILed with no escape hatch before this restriction. This is the same
"single-word targets are ordinary English" trade the Decision section already makes for the bare
form, extended to notation because the body genre has no boundary-sentence signal to fall back on;
- **a bare hyphenated word after any arrow is not notation.** The description gate's own bare-arrow
sweep (`NOTATION_ARROW`) reads ordinary process-chain prose as a route: `caveman`'s "Inline obj
prop -> new ref -> re-render." dangled to `re-render` under it. The body gate uses `ARROW_MARKED`
instead, which requires the target to be backticked or slash-prefixed — true of the one real
historical target (`` -> `to-prd` ``, confirmed against `03abcff`'s diff), so this costs no real
coverage;
- a target immediately preceded by `<` is a closing tag (`</what-to-do>`, `<supporting-info>` — this
repo's own `grill-with-docs/SKILL.md` uses these as prompt section delimiters), not `/name`
notation, and is discarded on that basis alone.
Both consumers — `scripts/skill-size-check.sh` and `factory-audit/scripts/lib-checks-skill.sh` —
call the shared functions independently over the same `known_targets()` universe the description
check already computed, so a body target folds into the existing "DID NOT RUN" INFO tier rather than
adding a second one. `tests/test-adr0020-targets.sh` pins the two live true positives, all three
guards above, and the fenced-code-block mask; the corpus-wide dangling assertion now covers body
targets the same way it already covered description ones. `docs/spec/gates.md`'s "Body-level routing
targets" section states the enforced shape in full.
## Consequences
**Editing any non-compliant skill now requires retrofitting it first.** At decision time, 30 of 39
+78 -15
View File
@@ -360,6 +360,68 @@ at a real sentence end. **Read the second bullet forward as well as back:** a ba
after a dotted filename is now extracted, resolved, and a blocking ERROR when it dangles, where the
same clause used to pass unchecked in silence.
### Body-level routing targets (issue #124)
Everything above resolves targets named in the **description** — the one field `boundary_targets()`
and `unresolved_targets()` read. Until issue #124, a target named in the **body** — a dispatch table
or a "run X" step, both routine in a 900-word procedure — was checked by nothing: `bin/write-docs`
routed twice to a deleted `to-prd` skill and `bin/triage` told an agent to run a nonexistent
`/setup-matt-pocock-skills`, and both were found by reading, not by any gate (fixed in `03abcff`;
the gate itself is the ask this section documents).
`body_targets()` / `unresolved_body_targets()` (`lib-boundary-resolver.sh`) are a **separate,
narrower** extractor, not a reuse of the description one at wider scope. A body is dispatch-table
and procedure prose, not a one-to-three-sentence routing clause, so `BOUNDARY_MARKER`, the follower
test and in-sentence corroboration all misfire on it in both directions — under-firing on a table
row that carries no "do not"/"instead", over-firing on a procedure step that names a file, a CLI verb
or a config key exactly the way a route names a skill. So the body gate reads only **notation**,
already the description gate's own "always blocks" tier, and nothing softer:
| Form | Pattern | Requires |
|---|---|---|
| `/name` | `NOTATION_SLASH` | a hyphen in `name`; not preceded by `<` |
| `-> name` / `→ name` | `ARROW_MARKED` | the name **backticked or slash-prefixed** — `NOTATION_ARROW`'s bare form is not used here |
Both constraints exist because the corpus, not intuition, said so — each is a real false positive
this gate produced once and was narrowed to remove:
- **No SUGGESTION tier, no continuation, one arrow per target.** Both forms are notation, and
notation is unconditionally blocking — there is no ambiguous prose reading left to soften, so
there is nothing to report at a softer tier. `CONT_MARKED`/`CONT_ANY` are not run either, so
`-> \`a\` or \`b\`` resolves only `a`, same as the one-arrow-one-target convention **#107** already
states for descriptions — enforced here by construction instead of by a second SUGGESTION.
- **A bare hyphenated word after any arrow is not notation here.** `NOTATION_ARROW` (used for the
description gate's own `Not X -> name` sweep) matches a bare `-> name` unconditionally, and a body
is full of ordinary arrow prose that is not a route: `caveman`'s own `Inline obj prop -> new ref ->
re-render.` read as a dangling route to `re-render` under that pattern. `ARROW_MARKED` requires the
target to be backticked or slash-prefixed, which the one real historical target (`` -> `to-prd` ``,
per `03abcff`'s diff) already was, so the narrowing costs no real coverage.
- **A single-word target is discarded, even in notation.** `` `/fork` `` (`forge/SKILL.md`,
contrasting `context: fork` with Claude Code's own `/fork` subagent command) and `` `/name` ``
(`skill-author/SKILL.md`, "the user types `/name`" — a placeholder for the skill's *own* name, not
a route) are both real corpus citations of a tool or a placeholder, not routes, and both hard-FAILed
with no escape hatch before the hyphen requirement was added. This is a real, accepted recall loss:
a body dispatch entry to a genuinely single-word skill (`forge`, `research`, `triage`, `tdd`,
`prototype`) cannot be checked through this extractor. Same trade the description gate already
makes for the *bare* form (the known gap above), extended here to notation as well because the body
genre has no boundary-sentence signal to lean on instead.
- **A name immediately preceded by `<` is a closing tag, not a route.** `grill-with-docs/SKILL.md`
uses XML-style prompt delimiters (`<what-to-do>...</what-to-do>`, `<supporting-info>...`), and
`</what-to-do>` is indistinguishable from `/what-to-do` notation by every other rule above. No route
is ever written directly after `<` in this corpus, so the guard costs nothing else.
Fenced code blocks are masked first (`mask_fenced()`, the same masking `gotcha_stats()` and the
references/-pointer check already use): an illustrative ` ```/some-skill``` ` in `skill-author` or
`factory-audit` — which document this very notation — is not a live dispatch entry.
Both consumers agree by construction: `scripts/skill-size-check.sh` and
`factory-audit/scripts/lib-checks-skill.sh` each call `body_targets()`/`unresolved_body_targets()`
independently, over the same `known_targets()` universe the description check already computed, so
the "DID NOT RUN" INFO tier covers both description and body targets in one message rather than
firing twice. `tests/test-adr0020-targets.sh`'s "body-level routing targets (issue #124)" section
pins both the two live true positives and every guard above; the corpus-wide dangling assertion
(`EXPECTED_DANGLING`) covers body targets the same way it already covered description ones.
### SUGGESTION-only checks
Deterministic to measure, judgment to act on:
@@ -1217,24 +1279,25 @@ point only). Machine-specific settings go in the gitignored `.claude/settings.lo
does not deploy and the replay does not compare; shared enforcement belongs in
`.pre-commit-config.yaml`.
### Why it is excluded from `pretty-format-json`
It is in the **second and last alternation** in that hook's `exclude:` pattern, and that alternation
is the only one there for a reason other than "generated manifest". Mind which number you are
quoting: the pattern is `^(\.claude-plugin/marketplace\.json|\.claude/(settings|apm-hooks)\.json)$`
— **two top-level alternations, expanding to three real tracked files**:
`.claude-plugin/marketplace.json`, this one, and its committed `.claude/apm-hooks.json` sidecar,
which is apm output under the same byte-for-byte replay and is excluded for the same reason.
### Why `pretty-format-json` runs with `--no-sort-keys`
`pretty-format-json --autofix` sorts object keys unless `--no-sort-keys` is passed, while apm's hook
integrator emits insertion order (`matcher` before `hooks`, `type` before `command`). Leaving the
file in that hook's scope therefore rewrites apm's output into a form apm would never produce on the
way into **every** commit, and `apm-audit-ci` then reports permanent drift on a file with an empty
`git diff` — exactly what happened when the `SessionStart` hook first landed in `2e395a4`. Re-running
`apm install` fixes the file; leaving it in scope would re-break it on the very commit carrying the
fix.
integrator emits insertion order (`matcher` before `hooks`, `type` before `command`). In scope with
the default, the formatter rewrites apm's output into a form apm would never produce on the way into
**every** commit, and `apm-audit-ci` then reports permanent drift on a file with an empty `git diff`
— exactly what happened when the `SessionStart` hook first landed in `2e395a4` (#102).
**Load-bearing. Do not tidy it out of that list** (see `LESSONS.md`, 2026-08-14).
The hook now passes `--no-sort-keys`, so this file and its committed `.claude/apm-hooks.json` sidecar
(apm output under the same byte-for-byte replay) need **no exclude**: the formatter's default 2-space
indent already matches apm's, and with insertion order kept they round-trip untouched. No dedicated
test pins this: dropping `--no-sort-keys` surfaces at pre-push as `apm-audit-ci` drift on
`.claude/settings.json`, which is the same gate that caught the original failure.
**Load-bearing. Do not remove `--no-sort-keys`.** `.claude-plugin/marketplace.json` is the one path
still in that hook's `exclude:`: it carries literal em dashes that the formatter re-escapes to
`\u2014`, which `--no-ensure-ascii` would stop but for every JSON file. This closes the JSON case
only; a new tool-owned file in the scope of another autofixer is still caught only by `apm-audit-ci`
drift after the fact, not by a derived gate.
## Pushing without a network
+2 -2
View File
@@ -9,7 +9,7 @@ apm is the only supported install path (ADR-0024). Declare this package in the c
```yaml
dependencies:
apm:
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: git@git.rkdr.net:Defame1297/holocron.git
path: plugins/bin
```
@@ -19,7 +19,7 @@ Then:
apm install
```
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `bin@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add [email protected]:Defame1297/holocron.git --name holocron`) gets you the `bin@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
**Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills — and Claude Code raises no error while doing it (ADR-0024).
+4 -4
View File
@@ -1,13 +1,13 @@
name: bin
version: 1.1.8
version: 1.1.9
description: Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.
author:
name: Defame1297
email: [email protected]
url: https://git.dev.rkdr.net/Defame1297/
url: https://git.rkdr.net/Defame1297/
license: MIT
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/bin
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/bin
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/bin
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/bin
keywords:
- utility
- diagnostics
+2 -2
View File
@@ -9,7 +9,7 @@ apm is the only supported install path (ADR-0024). Declare this package in the c
```yaml
dependencies:
apm:
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: git@git.rkdr.net:Defame1297/holocron.git
path: plugins/core
```
@@ -19,7 +19,7 @@ Then:
apm install
```
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `core@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add [email protected]:Defame1297/holocron.git --name holocron`) gets you the `core@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
**Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills — and Claude Code raises no error while doing it (ADR-0024).
+4 -4
View File
@@ -1,13 +1,13 @@
name: core
version: 1.1.3
version: 1.1.4
description: Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.
author:
name: Defame1297
email: [email protected]
url: https://git.dev.rkdr.net/Defame1297/
url: https://git.rkdr.net/Defame1297/
license: MIT
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/core
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/core
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/core
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/core
keywords:
- agents-md
- documentation
+2 -2
View File
@@ -9,7 +9,7 @@ apm is the only supported install path (ADR-0024). Declare this package in the c
```yaml
dependencies:
apm:
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: git@git.rkdr.net:Defame1297/holocron.git
path: plugins/git
```
@@ -19,7 +19,7 @@ Then:
apm install
```
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `git@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add [email protected]:Defame1297/holocron.git --name holocron`) gets you the `git@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
**Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills and zero agents — and Claude Code raises no error while doing it (ADR-0024).
+4 -4
View File
@@ -1,13 +1,13 @@
name: git
version: 1.3.8
version: 1.3.9
description: Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.
author:
name: Defame1297
email: [email protected]
url: https://git.dev.rkdr.net/Defame1297/
url: https://git.rkdr.net/Defame1297/
license: MIT
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/git
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/git
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/git
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/git
keywords:
- git
- vcs
+4 -4
View File
@@ -1,13 +1,13 @@
name: gitea
version: 1.3.9
version: 1.3.10
description: Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.
author:
name: Defame1297
email: [email protected]
url: https://git.dev.rkdr.net/Defame1297/
url: https://git.rkdr.net/Defame1297/
license: MIT
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/gitea
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/gitea
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/gitea
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/gitea
keywords:
- gitea
- issues
@@ -7,7 +7,7 @@ description: >
fixes -> agent-author.
allowed-tools: Bash Read
metadata:
version: "1.0.4"
version: "1.0.5"
category: factory
source_keys:
- agentskills-home
@@ -898,6 +898,103 @@ def unresolved_targets(description, known):
reported.add(name)
return sorted(blocking), sorted(reported - blocking)
# --- Body-level routing targets (issue #124) -------------------------------
# boundary_targets()/unresolved_targets() above are tuned for a description:
# one to three sentences, where BOUNDARY_MARKER, the follower test and
# in-sentence corroboration all exist to tell a routing sentence apart from
# ordinary prose about a hyphenated tool. A SKILL.md body is a different
# genre — up to 900 words of procedure and dispatch tables — where those same
# heuristics would misfire in both directions: a dispatch table rarely reads
# as a "boundary sentence" (under-fire), and a procedure step naming a file, a
# CLI verb or a config key looks exactly like a route (over-fire). Retuning
# the sentence-level heuristics for that genre is the hard half of this gate
# and is deliberately NOT attempted here — see the issue for why.
#
# So the body extractor takes the narrow route instead: only two EXPLICIT
# ROUTE NOTATION forms count, and each is measured against the real corpus
# (39 SKILL.md bodies) rather than assumed correct from the description gate's
# behaviour — a body is dense with prose that LOOKS like this notation and
# genuinely is not, in ways a one-to-three-sentence description never is:
#
# * ARROW_MARKED — `-> name` / `→ name` where the target is BACKTICKED or
# slash-prefixed (MARKED_TARGET). NOT NOTATION_ARROW, which matches a bare
# hyphenated word after any arrow: the corpus's own process-chain prose
# ("Inline obj prop -> new ref -> re-render.", caveman/SKILL.md) reads as
# a route under that pattern and does not under this one, because a
# process chain is never itself backticked or slash-prefixed. The one
# live true positive this was filed over, write-docs' "-> `to-prd`", IS
# backticked (03abcff's diff shows the original), so ARROW_MARKED still
# catches it losslessly.
# * NOTATION_SLASH — free-standing `/name`, unconditionally, the same
# pattern the description gate sweeps with. Two guards narrow it for body
# text specifically, each one measured against a real corpus false
# positive rather than hypothesised:
# - a name with NO hyphen is discarded. A real dispatch entry in this
# corpus always names a multi-word skill (`to-prd`,
# `setup-matt-pocock-skills`); a single bare or backticked word after
# a `/` is prose citing a CLI command, a Claude Code built-in or a
# placeholder — `` `/fork` `` (forge/SKILL.md, contrasting
# `context: fork` with Claude Code's own /fork subagent command) and
# `` `/name` `` (skill-author/SKILL.md, "the user types `/name`" —
# `name` is a placeholder for the skill's OWN name, not a route) are
# both real corpus hits this guard removes. This is a real recall
# loss — `/forge`, `/triage` and other single-word skill names are
# unreachable through this extractor — accepted deliberately, the
# same "start narrow" trade the issue itself recommends.
# - a name immediately preceded by `<` is discarded. An XML/HTML-style
# closing tag used as a prompt section delimiter — `</what-to-do>`,
# `</supporting-info>` (grill-with-docs/SKILL.md) — is indistinguishable
# from `/what-to-do` notation by every other rule in this pattern; no
# route is ever written directly after `<` in this corpus, so the
# guard costs nothing else.
#
# Every surviving hit is unconditionally blocking: both forms are explicit
# notation with the ambiguous single-word and closing-tag readings already
# removed, so there is no SUGGESTION tier here — that tier exists to soften
# an ambiguous prose form, and none is admitted at this point.
#
# No conjunction continuation (CONT_*) either: `-> \`to-prd\` or \`grill-me\``
# resolves only `to-prd`, the same one-arrow-one-target convention
# multi_target_arrow_clauses() already enforces on descriptions (issue #107),
# applied here by construction instead of by a second SUGGESTION.
def body_targets(body):
"""Every /name or -> `name` routing target named in a SKILL.md body.
Fenced code blocks are masked first, the same way gotcha_stats() and
missing_reference_pointers() mask them: a ```-fenced example quoting
`/some-skill` or `-> \`some-skill\`` as illustration is not a live
dispatch entry, and skill-author/factory-audit — which document this
very notation — are exactly the skills most likely to carry one.
"""
masked = mask_fenced(body)
names = set()
for match in NOTATION_SLASH.finditer(masked):
if match.start() > 0 and masked[match.start() - 1] == '<':
continue # </closing-tag>, not /route-notation
name = match.group(1)
if '-' in name:
names.add(name)
for match in ARROW_MARKED.finditer(masked):
name, _, _ = _first(match)
if name and '-' in name:
names.add(name)
return sorted(names)
def unresolved_body_targets(body, known):
"""Body routing targets (notation only) that resolve to nothing.
Unlike unresolved_targets(), this has one outcome, not two: every name
body_targets() finds is already route notation, and notation always
blocks. `known` is the resolved universe from known_targets(); passing an
empty set is not meaningful — callers check for that first and decline
out loud instead, exactly as they do for the description gate.
"""
return sorted(name for name in body_targets(body)
if normalize_target(name) not in known)
# --- Frontmatter ----------------------------------------------------------
# Tolerant on the way in, HARD-FAILING on the way out. A UTF-8 BOM, a leading
# blank line, trailing whitespace after either `---`, or CRLF line endings all
@@ -443,15 +443,23 @@ elif desc:
# derived from this script's own path, and — when an authoring root exists — it
# never reads a deployed .claude/ tree, so a fresh clone and a machine that has
# run `apm install` return the same verdict. See the shared resolver's header.
if desc:
routing_targets = boundary_targets(desc)
known = known_targets(skill_dir) if routing_targets else set()
if routing_targets and not known:
routing_targets = boundary_targets(desc) if desc else []
# Body-level targets (issue #124): notation only (`/name`, `-> name`), so
# every hit is unconditionally blocking — see the shared resolver's
# body_targets() header for why the description gate's SUGGESTION tier has
# no counterpart here. Read regardless of `desc`: a body dispatch table can
# carry a broken route even when the description carries none.
body_routing_targets = body_targets(body)
if routing_targets or body_routing_targets:
known = known_targets(skill_dir)
if not known:
unchecked = sorted(set(routing_targets) | set(body_routing_targets))
info(f"boundary-target resolution DID NOT RUN — no skill universe could be "
f"determined for this path (no authoring root above it, no apm package "
f"root, no declared apm dependencies, no deployed .claude/ or .agents/ "
f"tree). Unchecked target(s): {', '.join(routing_targets)}")
elif routing_targets:
f"tree). Unchecked target(s): {', '.join(unchecked)}")
else:
if routing_targets:
# blocking vs reported: a target only earns a FAIL when it is written in
# route notation or its own sentence corroborates it by naming another
# target that resolves. See the shared resolver's CORROBORATION note.
@@ -476,6 +484,15 @@ if desc:
resolved = [t for t in routing_targets if normalize_target(t) in known]
ok(f"{len(resolved)} of {len(routing_targets)} boundary target(s) resolve: "
f"{', '.join(resolved) if resolved else '(none)'}")
unresolved_body = unresolved_body_targets(body, known)
for target in unresolved_body:
fail(f"body routes to '{target}' (`/{target}` or `-> {target}` notation), which "
f"resolves to no skill or agent in this monorepo, in this package, or in a "
f"package it declares in apm.yml dependencies.apm — a dispatch table or "
f"\"run X\" step naming a non-existent target sends the agent nowhere")
if body_routing_targets and not unresolved_body:
ok(f"{len(body_routing_targets)} of {len(body_routing_targets)} body routing "
f"target(s) resolve: {', '.join(body_routing_targets)}")
# Body unfilled placeholders
fill_matches = PLACEHOLDER_RE.findall(body)
+7 -11
View File
@@ -1,31 +1,27 @@
---
name: forge
description: >
Use when the user wants to build or improve something but has not yet named
the artifact type — skill, agent, plugin, or marketplace entry; "not sure if
this should be a skill or a plugin", "I have an idea but don't know where it
belongs". Routes to the matching author skill. Do not use when the type is
already named — invoke `skill-author`, `agent-author` or `apm-workflow`
directly.
Use when the user wants to build or improve something without naming the
artifact type ("not sure if this should be a skill or a plugin"). Not a named
skill -> `skill-author`. Not a named agent -> `agent-author`. Not a named
plugin -> `apm-workflow`.
metadata:
version: "1.0.1"
version: "1.0.2"
category: factory
source_keys:
- claude-code-subagents-docs
- context7-websites-code-claude
- agentskills-spec
---
## Gotchas
- forge is an optional guided entry point, not a gate — `skill-author`, `agent-author`, `factory-audit` and `apm-workflow` all stay directly invokable, and forge never intercepts a direct call to one.
- Claude Code's skill-level `context: fork` frontmatter field and the `/fork` subagent command are opposites despite the shared word: `context: fork` isolates (fresh context, no parent access), while `/fork` inherits the full conversation. The route reference each classification loads spends that distinction: `references/author-routes.md` chooses between the two, `references/apm-routes.md` rules the fork out.
## Step 1 — Grill the intent
Call `grill-with-docs` unless a grill session has already run and is available in the context.
`grill-with-docs` ships in a sibling plugin that kyberforge does not declare as an apm dependency, so it resolves in the authoring monorepo but can be absent where kyberforge is installed alone. If it does not resolve, grill inline yourself rather than skipping the step: what problem the artifact solves, who invokes it and how, what it must refuse, and which existing skill or plugin already owns part of the job. Say which path you took.
`grill-with-docs` ships in a sibling plugin kyberforge does not declare as an apm dependency, so it can be absent where kyberforge is installed alone. If it does not resolve, grill inline yourself rather than skipping the step: what problem the artifact solves, who invokes it and how, what it must refuse, and which existing skill or plugin already owns part of the job. Say which path you took.
Grilling regularly overturns the artifact type assumed at the start, or splits one idea into several artifacts, so it runs before classification rather than confirming it. Run it inline in the current conversation — grilling is interactive and a subagent cannot hold the back-and-forth.
@@ -42,7 +38,7 @@ Match the grilled intent against exactly one row — or more than one, if the in
The table classifies what to build, not how to run it: a one-off task that merely needs an isolated or context-inheriting run is not an artifact and has no row here. If the intent stays genuinely ambiguous between rows after grilling, ask the user rather than guessing.
A real artifact that matches no row — a hook, an MCP server, an AGENTS.md, a research doc — has no route here. Say so, hand the user the skill that does own it, and never bend it into a row to make the table fit.
An artifact that matches no row — a hook, an MCP server, an AGENTS.md, a research doc — has no route here. Say so, hand the user the skill that owns it, and never bend it into a row.
When the intent spans several rows, chain the routes in dependency order — an artifact that must exist on disk before another skill can target it goes first, so `apm-workflow` scaffolds the plugin directory before `skill-author` scaffolds a skill inside it.
@@ -1,6 +1,7 @@
---
source_keys:
- claude-code-subagents-docs
- context7-websites-code-claude
---
# Routing a skill or agent to its author skill
@@ -10,6 +11,13 @@ definition. Route a skill to `skill-author` and an agent to `agent-author`. The
differ on the author skill only — both verify the result with `factory-audit`, which detects the
artifact type itself — and everything below applies to both.
## Gotcha: `context: fork` is not `/fork`
Claude Code's skill-level `context: fork` frontmatter field and the `/fork` subagent command are
opposites despite the shared word: `context: fork` isolates (fresh context, no parent access),
while `/fork` inherits the full conversation. The fork-versus-inline choice below is about `/fork`.
`references/apm-routes.md` rules the fork out entirely.
## Choose fork or inline
Default to a **fork subagent**. It inherits the full grilled-intent conversation, so the author
@@ -12,8 +12,8 @@
- **URL:** context7:/websites/code_claude
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code documentation site indexed by Context7 — confirms the `context: fork` skill-level frontmatter field means isolated/fresh execution, the opposite of what the `/fork` subagent command does (inherits conversation). Informs the Gotchas entry in `SKILL.md` warning against conflating the two; nothing else in this skill draws on it, and no `references/` file mentions the `context: fork` field.
- **Contributing files:** SKILL.md
- **Description:** Official Claude Code documentation site indexed by Context7 — confirms the `context: fork` skill-level frontmatter field means isolated/fresh execution, the opposite of what the `/fork` subagent command does (inherits conversation). Informs the `context: fork` gotcha in `references/author-routes.md` warning against conflating the two; nothing else in this skill draws on it.
- **Contributing files:** references/author-routes.md
- **Status:** `extracted`
## claude-code-plugins-docs
+2 -2
View File
@@ -9,7 +9,7 @@ apm is the only supported install path (ADR-0024). Declare this package in the c
```yaml
dependencies:
apm:
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
- git: git@git.rkdr.net:Defame1297/holocron.git
path: plugins/kyberforge
```
@@ -19,7 +19,7 @@ Then:
apm install
```
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `kyberforge@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add [email protected]:Defame1297/holocron.git --name holocron`) gets you the `kyberforge@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
**Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills, agents and hooks — and Claude Code raises no error while doing it (ADR-0024).
+4 -4
View File
@@ -1,13 +1,13 @@
name: kyberforge
version: 2.0.0
version: 2.0.2
description: Skills and agents for creating, maintaining, and managing an apm plugin marketplace for Claude Code and GitHub Copilot.
author:
name: Defame1297
email: [email protected]
url: https://git.dev.rkdr.net/Defame1297/
url: https://git.rkdr.net/Defame1297/
license: MIT
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge
keywords:
- marketplace
- plugin
+4 -4
View File
@@ -1,13 +1,13 @@
name: lint
version: 1.1.8
version: 1.1.9
description: Skills and agents for configuring and running linters.
author:
name: Defame1297
email: [email protected]
url: https://git.dev.rkdr.net/Defame1297/
url: https://git.rkdr.net/Defame1297/
license: MIT
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/lint
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/lint
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/lint
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/lint
keywords:
- lint
- style
+4 -4
View File
@@ -1,13 +1,13 @@
name: onedev
version: 0.1.0
version: 0.1.1
description: Skills and agents for working with a OneDev forge through the TOD CLI — the forge's own objects, as distinct from the local git clone.
author:
name: Defame1297
email: [email protected]
url: https://git.dev.rkdr.net/Defame1297/
url: https://git.rkdr.net/Defame1297/
license: MIT
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/onedev
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/onedev
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/onedev
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/onedev
keywords:
- onedev
- tod
+14 -2
View File
@@ -444,9 +444,14 @@ for path in files:
"\"Not X -> %s. Not Y -> %s.\"" % (path, first, second, first, second))
targets = boundary_targets(desc)
if targets:
# Body-level targets (issue #124): notation only (`/name`, `-> name`), so
# every hit is unconditionally blocking — see body_targets()'s header for
# why the description gate's SUGGESTION tier has no counterpart here.
body_route_names = body_targets(body)
if targets or body_route_names:
known = known_targets(skill_dir)
if known:
if targets:
blocking, reported = unresolved_targets(desc, known)
for target in blocking:
error("%s: description routes to '%s', which does not resolve to a skill "
@@ -462,12 +467,19 @@ for path in files:
"so this is equally likely to be a tool, a file format or an English "
"compound. If it IS a route, write it as `/%s` or `-> %s` and it will "
"be checked properly." % (path, target, target, target))
for target in unresolved_body_targets(body, known):
error("%s: body routes to '%s' (`/%s` or `-> %s` notation), which does not "
"resolve to a skill or agent in this monorepo, in this package, or in a "
"package it declares in apm.yml dependencies.apm (ADR-0020). A dispatch "
"table or \"run X\" step naming a non-existent target sends the agent "
"nowhere." % (path, target, target, target))
else:
unchecked = sorted(set(targets) | set(body_route_names))
info("%s: boundary-target resolution DID NOT RUN — no skill universe "
"could be determined for this path (no authoring root above it, no "
"apm package root, no declared apm dependencies, no deployed "
".claude/ or .agents/ tree). Unchecked target(s): %s"
% (path, ", ".join(targets)))
% (path, ", ".join(unchecked)))
sys.exit(1 if failed else 0)
SSC_CHECKS_PY
+13 -1
View File
@@ -46,6 +46,17 @@ fi
# which is ADR-0024 consequence 2 arriving here. Keeping the exclusion now is
# what stops that landing as a mystery double-run on the merge that enables it.
#
# build/ is excluded for the same reason again, one layer further out: `apm
# pack` stages a full copy of a package's tree (including its skills' tests/
# directories) under build/<package>-<version>/ before archiving it. Those
# staged .bats files carry the same six-levels-up REPO_ROOT walk-up as any
# other copy, which resolves past this repo's actual root and fails on a
# missing bats-support helper -- the same failure mode apm_modules/ and
# .claude/skills/ above already guard against, just from a different apm
# subcommand. build/ is gitignored and regenerated on demand, so nothing here
# depends on its contents; the exclusion only stops a stray local `apm pack`
# output from being discovered and double-run.
#
# The walk runs from inside REPO_ROOT so the exclusions match paths RELATIVE to
# it, the same universe the `git ls-files` grep below sees. Matched against
# absolute paths, `*/.claude/worktrees/*` excluded every file whenever the
@@ -61,6 +72,7 @@ done < <(
-not -path "*/.claude/worktrees/*" \
-not -path "*/apm_modules/*" \
-not -path "*/.claude/skills/*" \
-not -path "*/build/*" \
| sort
)
@@ -99,7 +111,7 @@ if [[ -n "$GIT_TOPLEVEL" && "$GIT_TOPLEVEL" == "$REPO_ROOT" ]]; then
[[ -n "$f" ]] && EXPECTED_FILES+=("$REPO_ROOT/$f")
done < <(
git -C "$REPO_ROOT" ls-files -- '*.bats' \
| grep -Ev '(^|/)tests/bats/|(^|/)test_helper/|(^|/)\.claude/worktrees/|(^|/)apm_modules/|(^|/)\.claude/skills/' \
| grep -Ev '(^|/)tests/bats/|(^|/)test_helper/|(^|/)\.claude/worktrees/|(^|/)apm_modules/|(^|/)\.claude/skills/|(^|/)build/' \
| sort || true
)
else
+111
View File
@@ -961,6 +961,117 @@ else
fail "an attributive target naming a REAL skill produced output (exit $ATTR_RC): $ATTR_OUT"
fi
# ---------------------------------------------------------------------------
# 3. Body-level routing targets (issue #124)
# ---------------------------------------------------------------------------
# boundary_targets()/unresolved_targets() are the DESCRIPTION gate, exercised
# above. body_targets()/unresolved_body_targets() are the separate, narrower
# extractor added for issue #124: a SKILL.md body is dispatch-table and
# procedure prose, not a one-to-three-sentence routing clause, so the body
# extractor takes only /name and -> `name` NOTATION (never the bare-prose
# forms the description gate also reads), and even within notation, a target
# must be hyphenated and must not be a `<tag` immediately before the `/`.
# Every fixture is built inside a real plugin tree (BODY_ROOT), same as
# section 2 above, so the resolver actually runs instead of declining.
echo ""
echo "--- body-level routing targets (issue #124) ---"
BODY_ROOT="$TMPDIR_T/body"
write_skill "$BODY_ROOT/plugins/p/.apm/skills/sibling-skill" sibling-skill \
"Use when doing the other thing. Do not use for anything else."
# write_skill_body <skill-dir> <name> <body>
write_skill_body() {
mkdir -p "$1"
{
echo "---"
echo "name: $2"
echo "description: Use when doing the thing. Do not use for anything else."
echo "metadata:"
echo " version: \"1.0.0\""
echo "---"
echo ""
printf '%s\n' "$3"
} > "$1/SKILL.md"
}
# body_case <slug> <expect: silent|errors> <needle> <body>
body_case() {
local slug="$1" mode="$2" needle="$3" body="$4" out status=0
write_skill_body "$BODY_ROOT/plugins/p/.apm/skills/$slug" "$slug" "$body"
set +e
out="$(bash "$HOOK" "$BODY_ROOT/plugins/p/.apm/skills/$slug/SKILL.md" 2>&1)"
status=$?
set -e
if [[ "$out" == *"DID NOT RUN"* ]]; then
fail "body \"$body\" — the resolver declined, so this case asserts nothing about extraction: $out"
return
fi
case "$mode" in
silent)
if [[ $status -eq 0 && -z "$out" ]]; then
pass "not a dangling body target: \"$body\""
else
fail "body \"$body\" (exit $status, output: ${out:-<empty>})"
fi
;;
errors)
if [[ $status -ne 0 && "$out" == *"$needle"* ]]; then
pass "dangling body target caught: \"$body\""
else
fail "body \"$body\" should have ERRORed with $needle (exit $status, output: ${out:-<empty>})"
fi
;;
esac
}
# The two live true positives the issue was filed over, at fixture scale:
# a bare/backticked `/name` and a backticked `-> \`name\``.
body_case body-slash-dangling errors "body routes to 'no-such-body-skill'" \
"Run \`/no-such-body-skill\` if the config is missing."
body_case body-slash-resolves silent "" \
"Run \`/sibling-skill\` if the config is missing."
body_case body-arrow-dangling errors "body routes to 'no-such-arrow-body'" \
"- User wants X -> \`no-such-arrow-body\`"
body_case body-arrow-resolves silent "" \
"- User wants X -> \`sibling-skill\`"
# No conjunction continuation: only the FIRST target after an arrow is ever
# read, so a dangling SECOND name is silently uncounted rather than reported
# — the same one-arrow-one-target convention issue #107 enforces on
# descriptions (there, at SUGGESTION tier; here, by construction, since the
# body gate has no SUGGESTION tier at all).
body_case body-arrow-no-continuation silent "" \
"- User wants X -> \`sibling-skill\` or \`no-such-uncounted-target\`"
# The single-word guard: a real corpus false positive removed by requiring a
# hyphen. `` `/fork` `` (forge/SKILL.md) and `` `/name` `` (skill-author/SKILL.md)
# are both single-word citations of a tool or a placeholder, not routes, and
# both would otherwise have hard-FAILed with no escape hatch.
body_case body-slash-single-word-guard silent "" \
"See \`/fork\` for how the two differ."
# The closing-tag guard: an XML/HTML-style section delimiter used as a prompt
# marker (grill-with-docs/SKILL.md's <what-to-do>...</what-to-do>) is
# indistinguishable from /route notation by every other rule in the pattern —
# a `<` immediately before the `/` is the one signal that tells them apart.
body_case body-closing-tag-guard silent "" \
$'<what-to-do>\nDo the thing.\n</what-to-do>'
# The bare-arrow guard: NOTATION_ARROW (bare hyphenated word after any arrow)
# is deliberately NOT used here, only ARROW_MARKED (backticked or
# slash-prefixed). caveman/SKILL.md's own process chain, "Inline obj prop ->
# new ref -> re-render.", is real corpus prose this guard exists for — an
# unbacked, unresolvable name after an arrow must stay silent, not become a
# hard-blocking dangling-target FAIL with no suppression mechanism.
body_case body-arrow-bare-not-notation silent "" \
"Reproduce -> minimise -> no-such-bare-chain-target."
# Fenced code blocks are masked, same as gotcha_stats() and
# missing_reference_pointers() mask them: an illustrative example is not a
# live dispatch entry.
body_case body-fenced-example silent "" \
$'```\nRun /no-such-fenced-skill instead.\n```'
echo ""
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]]
+34
View File
@@ -476,6 +476,40 @@ else
fail "the plan-shortfall run failed with the wrong count: $FAKE_OUT"
fi
# --- 12. A build/ directory (apm pack's staging output) is excluded, the same
# way apm_modules/ and .claude/skills/ above are. A stray local `apm pack` run
# leaves build/<pkg>-<version>/ on disk holding a full copy of every packaged
# skill's tests/ directory, gitignored and regenerable, but discoverable by a
# bare `find` all the same. Those staged .bats files carry the same
# several-levels-up REPO_ROOT walk-up as any other copy, which overshoots this
# fixture's root, so an unexcluded build/ turns into the same
# bats-support-not-found failure apm_modules/ and .claude/skills/ already guard
# against -- this was caught live with 423 duplicate failures against a real
# checkout holding a stray build/holocron-*/ from an earlier `apm pack`.
echo ""
echo "--- a build/ directory holding staged .bats copies is excluded ---"
DIR12="$(make_fake_repo)"
FIXTURES+=("$DIR12")
seed_bats_files "$DIR12"
mkdir -p "$DIR12/build/some-pkg-1.0.0/tests"
printf '@test "staged" { false; }\n' > "$DIR12/build/some-pkg-1.0.0/tests/staged.bats"
install_stub_bats "$DIR12" <<'EOF'
#!/usr/bin/env bash
echo "1..1"
echo "ok 1 first"
exit 0
EOF
run_fake "$DIR12"
if [[ $FAKE_RC -ne 0 ]]; then
fail "a tree holding a build/ directory failed the run: $FAKE_OUT"
elif grep -q "build/some-pkg-1.0.0" <<< "$FAKE_OUT"; then
fail "a .bats file staged under build/ was discovered and run: $FAKE_OUT"
elif grep -q "^2 tests, 0 failures$" <<< "$FAKE_OUT"; then
pass "a build/ directory's staged .bats copies are excluded from discovery"
else
fail "the build/-exclusion run passed with an unexpected count: $FAKE_OUT"
fi
echo ""
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]]