Commit Graph

23 Commits

Author SHA1 Message Date
4aab9d327c refactor(kyberforge): retrofit forge to the ADR-0020 contract
Description 648 -> 387 chars, body 1093 -> 541 words. This was the last
body FAIL in the 39-skill corpus.

The body was not trimmed to fit. forge routes four artifact types that a
single invocation classifies between, so the contract requires a dispatch
table plus the gates common to every route, with each route self-contained
in references/. Adds references/author-routes.md (skill and agent),
references/apm-routes.md (plugin and marketplace entry) and
references/version-bump.md. Skill and agent share one file: they differ on
one axis only, which audit skill verifies the result.

Fixes three defects the first pass introduced or relocated:

- references/apm-routes.md claimed `apm audit` "already runs inside
  apm-workflow's own flow" and told the agent to confirm it ran clean.
  apm-workflow dispatches audit as its own row; the configure and
  marketplace rows never reach it. That was the only completion check
  these routes had, and it could never be satisfied. Replaced with a
  manual read-back the agent performs itself.
- "Read only the reference file" forbade the multi-artifact case the same
  body documents two lines later, and ADR-0011 records eight artifacts
  authored in one pass.
- The announce gate became a closing gate, reachable only after the
  invocation it was meant to precede. Moved to the end of Step 2.

Also restores the artifact enumeration to the plugin row, normalises to
bare unnamespaced skill names per AGENTS.md, adds a dispatch fallback for
artifacts matching no row, and corrects three provenance entries -- one
asserted a contribution that did not happen.

Refs #99

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWb5RQgCL1ye7cGp2RPb2u
2026-08-30 16:31:23 +00:00
1e4aab53a7 refactor(kyberforge): retrofit apm-workflow to the ADR-0020 contract
Description 817 -> 324 chars, body 421 -> 237 words, Gotchas 6 -> 2.

The five capability clauses, the second trigger register and the prose
boundary form go; one trigger clause, the indirect trigger and one
boundary clause remain. Four of six Gotchas move into the flow file that
every branch needing them already loads.

Two stay in the always-loaded body because a dispatch body must carry the
gates common to every branch, not just the dispatch table: the MCP secret
indirection rule, and the `apm experimental enable registries`
precondition. The first pass moved registries into references/configure.md
alone, which stranded it -- references/compile.md documents publishing to a
registry and references/install.md resolves dependencies through one, and
neither points at configure.md. Declaring a registry without the
precondition is a silent no-op, so the failure had no signal.

Also drops an unsourced claim the compression pass introduced (that apm
checks `type:` going forward -- no source supports it), corrects the MCP
rationale to install *or* runtime per configuration.md:98, and repoints
two apm-orchestrate back-references that pointed at body Gotchas which had
moved.

Refs #99

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWb5RQgCL1ye7cGp2RPb2u
2026-08-30 16:31:11 +00:00
7e80c09b13 fix(kyberforge): a dispatch table satisfies the reference-wiring rule
body-discipline.md required every reference load to use the literal
'If X, read references/file.md' form and called anything else a generic
pointer. ADR-0020's own cited dispatch exemplar, apm-workflow, uses a
bare table plus one closing line, so an author could not satisfy both --
and the rule reliably produced duplication in exactly the bodies the
contract exists to keep short.

Resolves #109 with its option 1: a table row already pairs a condition
with a target, so where a body dispatches, the table is the wiring. The
literal form is what a body needs when it loads a reference without a
table.

Two corrections to the issue as filed. There is no Vale conflict --
PaddingPhrase.yml only matches 'see references/ for more info' and never
fired on the exemplar, so this is a one-file prose fix and no rule
changes. And gitea-workflow carried the predicted duplication: a
three-row table restated underneath as three conditionals. Removed, body
227 -> 148 words.

Closes #109
2026-08-30 15:07:16 +00:00
a2ebdafc5e fix(skill-audit): pin the stale worked example and state its reachability precondition
The body-discipline rubric cited git-commits as it stood before the ADR-0020
retrofit -- twelve Gotchas, 387/1102 words, line numbers :31-:52. Every
figure was correct for that version and none survives in the current file,
so the example is now anchored to commit 5e23250 and marked not to be
refreshed against HEAD.

More than staleness: row four called the secrets Gotcha a paraphrase FAIL
because step 2 restated it. Wave 2 followed that reasoning, deleted the
always-loaded copy, and left the amend branch able to commit a credential
unchecked -- dispatch loads exactly one flow file. The paraphrase rule now
carries its missing precondition: delete a restating Gotcha only when the
surviving copy is reachable from every branch that needs it, and relocate
a multi-branch safety gate into the body rather than dropping it.
2026-08-30 13:22:55 +00:00
54d7bd80ba docs: rule host built-ins out of the routing target universe
Closes the second open design decision on PR #103. The `/compact` finding was
recorded as a false positive needing an allowlist or a suppression mechanism.
It is neither: the routing universe is the apm marketplace, so a target either
resolves to a skill or an agent or it does not resolve, and `/compact`,
`/clear` and `/init` are Claude Code slash commands with no counterpart in
Copilot CLI or Codex. `.apm/` source compiles for all three, so a
vendor-neutral description routing to one is a portability defect and the hard
FAIL is a true positive.

An allowlist was rejected for a concrete reason, not a stylistic one: it
answers a different question ("does this exist on some host?"), it cannot
answer that portably from a single source file, and it goes stale the next
time a host ships a command — reintroducing the same-commit-two-verdicts
failure ADR-0020 already closed for deployed trees.

Nothing is blocked today: zero of the 43 descriptions name a host built-in,
and an author who needs to mention one writes it un-slashed, which is not
route notation and carries no routing claim.

Recorded in ADR-0020 and in both author-facing contract references, so the
next agent reads the decision rather than "fixing" the gate.

ADR: 0020

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 20:45:19 +00:00
75a13c82f6 fix(kyberforge): scope corroboration to a real sentence boundary
A prose-form routing target blocks a commit only when its own sentence names
another target that resolves. That makes the sentence splitter part of the
ADR-0020 contract rather than an implementation detail, and the naive
"period, space, capital" rule got it wrong in both directions:

- OVER-SPLIT: `e.g. "..."` is not a sentence end, but the quote looks like a
  start. The clause was cut in half and the corroborator stranded on the far
  side, so a genuinely dangling target silently demoted to SUGGESTION — a
  measurement taken and then discarded, the vacuous-green shape this gate
  exists to prevent. Seven such splits are live in the current corpus.
- UNDER-SPLIT: a sentence opening with a code span or a lowercase skill name
  was not seen as a start, so two sentences merged and a resolving target
  vouched for an unresolvable one it never stood beside — a hard FAIL with no
  escape hatch, which is the exact failure corroboration was added to prevent.

The splitter now excludes the five abbreviations that occur in routing prose
and admits a backtick or lowercase letter as a sentence opener. Applied
byte-identically to all three copies of the shared resolver.

Verified zero-delta against the corpus: 37 ERROR / 58 SUGGESTION / 2 dangling
before and after, findings byte-identical. The exposure this closes is to the
descriptions #99 is about to rewrite, not to the ones already measured — which
is why the deferral reason recorded on PR #103 ("can move the documented corpus
counts") does not hold and the fix lands here rather than after the retrofit.

Three regression tests, one per direction plus the backtick opener, each proven
non-vacuous by reverting the splitter alone and watching it go red.

Refs: #99
ADR: 0020

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 20:44:58 +00:00
ede3f06689 fix(kyberforge): restore the authoring rules the ADR-0020 trim dropped
Diffing each retrofitted SKILL.md against its replacement references/ files found
rules that existed on main and now existed nowhere — relocated in intent, deleted in
fact. A trim that loses a rule is not progressive disclosure, it is data loss with a
smaller word count.

Three had no survivor. The least-privilege guidance for `tools` kept its mechanics
and lost the "restrict to what the agent needs" half, so the remaining text read as
encouragement to omit the field. The improve flow lost its regression check, so
nothing compared the closing audit against the pre-edit state and a PASS quietly
becoming a SUGGESTION went unnoticed — restored on both halves of the author pair,
since agent-author had dropped its equivalent too. And agent bodies lost "would the
agent get this wrong without it?", which mattered more than it looks: ADR-0020
deliberately sets no body word gate for agents, three of the four already sit
between 933 and 1,199 words, and the delegation check only fires on procedure a
skill already owns. That heuristic was the only brake left.

Two more were reachable only from the wrong scope. agent-author tells the reader to
load only the file for the resolved scope, but the mcp__ glob syntax for
disallowedTools and the five tools no subagent ever receives had both landed in
project-user-scope.md. disallowedTools is the ONLY permitted fence at plugin/APM
scope, so the scope that needs the syntax most could not reach it, and a plugin-scope
run could write a body telling the agent to ask the user a question.

Two documents were actively wrong rather than merely thin. agent-audit told auditors
that validate.sh resolves boundary targets for skills only; it runs at both scopes,
so the auditor was hand-resolving what the script had already decided and could
contradict it. And skill-audit routed to its script-troubleshooting reference
whenever validate.sh "fails" — but it exits 1 on ordinary content FAILs, the normal
outcome for the whole #99 population, so 1,302 words loaded on nearly every audit.
A context-budget regression inside the skill that enforces the context budget.

Finally, two illustrations taught the shape the gate ERRORs on, unfenced, while an
adjacent rubric called it a hard ERROR.

LESSONS.md records the reference-chain depth rule flipping from "one level deep" to
"two hops, never three". ADR-0020 is silent on it and the reversal rode entirely on
the diff; the looser rule is what mandatory dispatch requires.

Refs: #99
ADR: 0020

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W3iwF9ncfRZddGBxsMCYi
2026-08-16 19:49:38 +00:00
b0d6d08239 test: pin the nine ADR-0020 gate defects that shipped untested
Every defect fixed in f7cc279 was reachable because nothing asserted against it.
The gate had 43 assertions and none of them covered a consumer repo, a non-string
description, an unclosed fence, or the two spec ceilings. Each case below fails
against the pre-fix code and passes against the current one; every one was proved
non-vacuous by mutating a scratch copy of the script and watching the test go red,
independently twice.

The two that mattered most had no fixture anywhere. A consumer repo WITH .git is
the shape the resolver exists to serve, and only the no-.git case had ever been
tested, which is exactly why the blocker was invisible. And ADR-0020 says the
walk-up runs in two passes specifically so a nested .git cannot beat a plugins/
root further up — no fixture had ever placed a .git inside a plugin.

test-adr0020-differential.sh loses _non_adr_hook_error(). It excluded MAX_LINES and
MAX_WORDS from the cross-script comparison on the untested assumption that awk and
splitlines() agree. They do not, and the divergence stayed invisible for exactly as
long as the exclusion stood. The ceilings are now compared like any other rule.

Two existing assertions were repairs, not additions. The skill-improve probe had
been fixed by this very branch, so its iteration permanently took an
assertion-free SKIP that still counted as a pass; both branches now fail loudly and
each names the other file's pin so the two stay in step. And the yaml-none fixture
emitted `---/---`, which never matched the frontmatter pattern at all — it passed on
the bare word "frontmatter", present in both messages, while never reaching the
branch it was named for. Needles throughout that file now name their branch.

Refs: #99
ADR: 0020

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W3iwF9ncfRZddGBxsMCYi
2026-08-16 19:49:15 +00:00
f7cc27908c fix(kyberforge): close the vacuous-green and consumer-resolution defects
Review of the ADR-0020 gate found four ways it could exit 0 without measuring, and
one way it hard-failed a repo it had no business failing. On a gate shipping hot
with no baseline, a silent pass is the worst outcome available and a false block is
the second worst.

Consumer resolution was the blocker. _authoring_root() fell back to the nearest
.git, so it returned truthy in ANY git repo; _collect_authoring_root() then
contributed nothing and the deployed-tree branch was dead code in precisely the
consumer case it exists for. A consumer repo routing to an installed sibling got an
unblockable ERROR, and deleting .git "fixed" it. It now keys on which of the two
walk-up passes matched. A name-count delta was tried first and is wrong: a
single-plugin monorepo re-collects its own package and adds no new name, so the
delta reads zero and drags the deployed trees — including a global ~/.claude — back
into the universe. That reintroduces the install-dependence ADR-0020 forbids, one
layer down.

The three silent passes: an indented `---` inside a block scalar truncated the
frontmatter and reclassified the rest of the description as body; a non-string
description was str()-coerced, so `description: true` measured as the four-character
"True"; and an unterminated fence blanked the rest of the body, disabling the
ERROR-tier references/ check and the gotcha counts.

Two measurement defects came with them. The awk line/word counts discarded awk's
exit status, so an unreadable file passed both spec ceilings in total silence, and
awk NR/NF disagreed with the audit script's splitlines()/split() on Unicode
whitespace — the "fix one gate, get blocked by the other" bug, on the two axes the
differential test deliberately excluded. Both counts now run in the Python block
that already reads the file. A type error also no longer reports itself as a syntax
error.

Also: glob metacharacters in the checkout path silently disabled the resolver;
re.I was applied to some extraction patterns and not others; agent-audit missed
`tools:` written as a YAML block sequence, the shape Copilot files use; and a
nonexistent agent file raised a bare FileNotFoundError instead of a diagnostic.

The shared resolver block stays byte-identical across all three scripts. Corpus
output is unchanged — 26 description FAIL, 9 body FAIL, 2 dangling, 0 missing
references, 58 SUGGESTIONs — so no documented count moves.

Refs: #99
ADR: 0020

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W3iwF9ncfRZddGBxsMCYi
2026-08-16 19:48:56 +00:00
311e7cd22c fix(kyberforge): reconcile the authoring rules the ADR-0020 trim left disagreeing
Six defects, each one a place where two files that an author reads in the same
sitting told them different things — or where the trim dropped a rule and nothing
noticed because no gate covers prose.

**"Use proactively" contradicted itself across the pair.** All three agent
templates said to add it where the runtime should delegate unprompted, while
`agent-audit`'s `KyberforgeCopilot.ProactivePhrase` rule grades it a hard FAIL in
any `*.agent.md` — which is the Copilot half of every project/user pair *and* the
vendor-neutral plugin-scope file, since that compiles to a real Copilot agent
downstream. Following the template produced a file the repo's own gate rejects.
The phrase is now permitted in exactly one place, the Claude Code `.md`, and
`references/contract.md` carries the per-file table plus the consequence authors
ask about next: a pair whose CC half has it and whose Copilot half does not is
correct, because `agent-audit` checks that both halves describe the same job, not
that they match word for word.

**The output-schema rule contradicted itself inside one file.** `contract.md`
said any content only one branch reaches moves to `references/`, and then offered
an "Output format template" body pattern with no qualification. Stated once now,
so it is not re-litigated: an output schema stays in the body only when every flow
produces it and it is roughly 50 words or less. No third option.

**Gotchas tiers disagreed with the script.** `validate.sh` emits the entry count
through `suggest()` and exits 0, while `skill-author` and `skill-audit` both
called more than five entries a FAIL. Whether a given gotcha earns its place is
judgment, so the prose moves to the script's tier rather than the reverse. The
paraphrase rule stays a FAIL and is explicitly marked as the auditor's call — no
script detects it.

**The dispatch exemplar was cited at the wrong number.** `apm-workflow`'s body is
421 words; 554 is its whole-file count. Both `contract.md` and `body-discipline.md`
cited 554 while describing a body budget, so an author calibrating against the
exemplar overshot by ~30% — the exact whole-file/body-only conflation those two
sections exist to warn against, reproduced inside the warning.

**"Error handling" came back as a required body element.** It was one of four and
is the one that gets dropped, and dropping it is not neutral: an agent handed
malformed input with no instruction invents a recovery, and a subagent's invented
recovery is invisible to its caller until the output is wrong. Restored in
`agent-audit`'s rubric as a SUGGESTION, in `agent-author`'s contract and both
scope checklists as a required element, and as an `## Errors` section in all three
templates.

**`skill-author` Step 4 gains the one check the audit misses.** An empty body
reports `PASS SKILL.md body word count 0` — a word gate cannot tell "concise"
from "absent". Step 4 now hand-checks for a non-empty section, and its commit
verification is conditioned on actually being inside a git worktree, which a skill
under `~/.claude/skills/` is not.

Also here: absolute repo paths removed from `skill-author`'s SKILL.md and
contract.md in favour of naming the skill (`zoom-out`'s description is quoted
inline instead of pointed at), the boundary-target universe documented to match
the resolver, a two-hops-from-SKILL.md limit on reference chains, and
`new-agent.sh`'s next-steps output naming the description budget and the
deliberate absence of an agent body gate.

Refs: ADR-0020
2026-08-16 16:40:51 +00:00
2540e50fcc feat(kyberforge): give skill-author a procedure for the #99 retrofit
ADR-0020 shipped its gates hot with no baseline file, so 26 of 39 descriptions
and 9 of 39 bodies are over their FAIL tier and editing any of them for any
reason requires bringing the skill into contract first. `references/improve.md`
said exactly that and stopped there — it mandated a retrofit and supplied no
procedure for one.

Four dry-run retrofits confirmed what that costs. Asked the same questions —
what to cut first, when a body is two flows rather than one, what else has to
change alongside — they invented six to ten different answers, so the same skill
retrofitted twice produced two different skills and neither run could be reviewed
against anything.

`references/retrofit.md` fixes the answers: an ordered cut list ranked by tokens
removed against behaviour lost (inverting that order is how a retrofit deletes the
instruction the skill existed to carry), the test for whether a body holds two
mutually exclusive flows, the reference-file conventions, the collateral checklist
for `README.md` and `references/sources.md`, and a worked description retrofit.

It also states the trap the dry runs kept hitting: retrofit the skill in place,
inside its package. The boundary-target universe is built by walking up from the
file being checked, so a scratch copy has no authoring root above it, the check
prints `INFO ... DID NOT RUN`, and the run still exits 0 — a line that reads as a
pass and is not one. A retrofit signed off on a copy carries an unverified
boundary target into the corpus.

Loaded from the improve flow only when a budget is actually exceeded, so a routine
improvement pays nothing for it.

Refs: ADR-0020, #99
2026-08-16 16:40:19 +00:00
a85bdbed42 fix(kyberforge): restore skill-audit's script-failure fallback and E100 diagnostic
The ADR-0020 body trim took `skill-audit` from 2,623 body words to a dispatch
shape, and two things went out with it that were not padding.

The manual structural fallback was one. Its replacement was a single sentence
telling the auditor to report an INFO when `validate.sh` cannot run — so with no
`python3` or no PyYAML, `skill-audit` reported the gap honestly and then audited
nothing structural at all. Every ADR-0020 measurement, the whole-file ceilings,
the name-to-directory match, the `references/` pointer check and the script
hygiene checks silently left the audit. A skill's whole Structure dimension
hanging on one optional interpreter is the same vacuous-pass shape the gate
scripts were just fixed for, one layer up.

The `E100 Runtime error ... does not exist` diagnostic was the other. That exit
code means an explicit relative `--config` was passed to `vale-wrap.sh` while
vale itself was installed and working; without the note, Step 1's fallback reads
exit 2 as "vale unavailable" and downgrades the description, body-discipline and
patterns dimensions to full LLM judgment for a config error it could have fixed.
That misreading is already recorded in CONTEXT.md as the reason both audit skills
stopped passing `--config` at all.

Both are restored in `references/validation-scripts.md`, loaded only when a Step 1
script fails — so the body pays nothing for them on a clean run, which is what the
dispatch pattern is for. The file also carries the by-hand boundary-target
procedure and the three ways to misread the result, including that
`INFO ... DID NOT RUN` is not a pass.

`references/file-structure.md` gains the one sanctioned spelling for a cross-skill
reference. The possessive form (``skill-audit's references/validation-scripts.md``)
is the only spelling both rules accept: a full repo path is what that section
already forbids, and a bare `references/<file>.md` is now a hard ERROR from the
ADR-0020 pointer check, which requires the file to exist in the skill's *own*
directory. Without the rule the two constraints look mutually exclusive.

Refs: ADR-0020
2026-08-16 16:40:00 +00:00
b6e68e9a2b fix(kyberforge): close the vacuous-pass paths in the ADR-0020 gate scripts
Three ways the gates could report green having measured nothing. All three were
invisible to a passing test suite, because pre-commit prints nothing at all for a
hook that exits 0 — a gate that declines to check and a gate that checked and
passed produce the identical signal.

- A UTF-8 BOM, a leading blank line, a trailing space after a `---` marker or
  CRLF line endings defeated the `^---\n` frontmatter matcher. Every ADR-0020
  check was then skipped and the file passed: measured at the time, a
  550-character description with a 1,000-word body exited 0 behind a BOM.
  All four shapes are now tolerated, and frontmatter that genuinely cannot be
  parsed is a hard ERROR rather than a silent skip.
- An agent file with a valueless `description:` followed by another key let a
  line regex capture the *next* key, which looked non-empty, so the
  missing-or-empty branch never fired and every gate below it early-returned on
  the empty folded value — zero output, exit 0, on a blocking gate. The one
  field this contract is entirely about was the one field a gate could fail to
  notice was absent. Presence is now decided on the YAML-folded value and
  nowhere else, and a missing or empty description is a hard FAIL in all three
  validators.
- The hand-rolled frontmatter fallback disagreed with PyYAML across the FAIL
  boundary on folded scalars, so which reader happened to be available decided
  the verdict. A fallback that mis-parses a scalar shape reports a vacuous pass,
  which is worse than not running, so it is deleted: python3 and PyYAML are hard
  requirements that fail loudly with an install pointer.

Boundary-target resolution no longer derives its universe from its own location.
A `${BASH_SOURCE}`-relative repo root leaked this repo's 39-skill universe into
every consumer repo running the hook through pre-commit, so a consumer skill
routing to `skill-audit` resolved against a plugin it had never installed. The
interim form resolved through `.claude/` and `.agents/`, which are gitignored
`apm install` output — the same commit reported 2 dangling targets on a machine
that had run the install and 6 on a fresh clone. Resolution now walks up from the
file being checked to an authoring root (nearest ancestor holding
`plugins/*/.apm/{skills,agents}`, else the nearest `.git`, in two passes so a
nested `.git` cannot outrank a real monorepo root); the universe is every skill
and agent under `<root>/plugins/*/` plus the file's own apm package and that
package's declared `dependencies.apm`. Deployed trees are consulted only when no
authoring root exists at all — the consumer case. One commit now gets one verdict,
which a gate shipping hot with no baseline file has to.

Narrowed in the same pass: a routing target inferred from the prose boundary form
and corroborated by nothing else reports at SUGGESTION instead of blocking. A
blocking check with no escape hatch is the wrong trade when the inference from
prose is the weak part of it.

New deterministic checks, all previously untested or absent: every
`references/<file>.md` a body names must exist (ERROR — a broken pointer is not a
style opinion); a description with no boundary clause at all, a Gotchas section
over five entries, and a Gotchas section over 25% of the body are SUGGESTIONs.
Where no universe can be determined the target check prints `INFO ... DID NOT
RUN` rather than passing quietly. Each prose-scanning check needed its own
false-positive fix — a fenced example of a Gotchas section was being read as the
section itself — and those fixes are pinned rather than assumed.

The resolver is one block copied verbatim into all three scripts between
BEGIN/END markers, because a cache-installed plugin's scripts cannot read outside
their own plugin directory. Nothing asserted the copies were still identical; a
one-line edit to a single copy passed every constant-agreement assertion, since
constants are not what drifts.

Tests land here rather than in a later commit. The existing suites assert the old
behaviour and go red against these scripts, so splitting them would leave a commit
whose own `run-tests` pre-push gate fails in isolation.

Refs: ADR-0020
2026-08-16 16:39:29 +00:00
4a5c3c0cff feat(kyberforge): enforce the ADR-0020 context contract for skills and agents
Skill name+description pairs are preloaded into every session, costing
~6,200 tokens across 39 skills before any skill is invoked. The authoring
rules mandated that growth: skill-author:104 and description-quality.md:21
both required padding, while skill-author:102 (the deflating rule) had no
FAIL condition behind it.

Gates (blocking, no baseline file):
- description 250 chars SUGGESTION / 400 FAIL, measured on the folded
  YAML value
- body-only 600 words SUGGESTION / 900 FAIL, independent of the unchanged
  whole-file 2770-word / 500-line spec backstop
- every boundary-clause routing target must resolve to a real skill or
  agent; catches skill-improve, neuledge-context and gitea-labels
- agents take the description gates but deliberately no body gate; a test
  pins that absence

Vale: DescriptionOpener widened to ^This\b, new CompositionNote rule
banning architecture notes from descriptions. 10 hits, 0 false positives.

Kyberforge's own four skills retrofitted: descriptions 3,364 -> 938 chars
(-72%), bodies 8,306 -> 2,487 words (-70%), all via the apm-workflow
dispatch pattern. Fixes the skill-improve dangling route and the
agent-author misroute to manual review.

Also fixes a pre-existing false positive where any line-initial 'read '
was flagged as interactive input, which had already caused two scripts to
be rewritten around it.

Refs: ADR-0020
2026-08-14 21:13:13 +00:00
ae178a95a2 fix(kyberforge): detect a single stale package at SessionStart
apm prints "1 outdated dependency found" in the singular when exactly one
package is behind (apm_cli/commands/outdated.py). check-apm-current.sh
matched only "outdated dependencies found", so one stale package was
invisible: the hook exited 0 silently and no refresh ran. With six
packages merging independently, one-behind is the ordinary case, so the
freshness mechanism failed most often in the situation it exists for.

Three further defects in the same hook:

- The host timeout was below the script's own budget. hooks.json declared
  320s while the script allows `timeout 60` plus `timeout 300` = 360s, so
  a slow remote let the host kill the hook mid-update and leave
  .claude/skills/ half-deployed with nothing emitted. Now 380. A test
  asserts the invariant rather than the literal: it sums every `timeout N`
  parsed out of the script and requires hooks.json to exceed it, so
  changing either side alone fails.

- The lockfile guard was cwd-relative, so a session opened in a
  subdirectory no-opped silently and ran both apm calls against the wrong
  directory. Now anchored on CLAUDE_PROJECT_DIR, falling back to the cwd
  so the hook stays inert under a host that does not set it.

- Every assertion mocked apm, so the suite was green over code that could
  not detect its own most common trigger. That blind spot is what hid the
  singular/plural bug, and it is the same shape as the deleted post-push
  tests. The suite now stages a genuinely outdated dependency against a
  local git remote — offline, via url.<path>.insteadOf, so the
  pass-under-unshare property survives — runs the real `apm outdated`, and
  replays its output through the real hook. Reverting the grep to
  plural-only fails it.

23 -> 35 assertions. Each fix mutation-tested individually. kyberforge
stays at 1.5.0: it is untagged, so this changes what 1.5.0 ships rather
than superseding it, and executables.allow needs no edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 18:31:30 +00:00
dee56c506a feat(kyberforge): refresh the apm install at SessionStart, not at push
Why
---
ADR-0018 left deployed skills tracking the remote default branch with nothing
watching for drift. The mechanism that was supposed to cover this,
scripts/git-hooks/post-push, could never have worked: git has no client-side
post-push hook. install.sh copied it into .git/hooks/ so it looked installed,
and it had never once fired. Issue #78 reported it as skipping the gitea
plugin; it was skipping everything.

Refreshing on push was also the wrong shape. Your install goes stale when
someone else merges, so a push of your own is neither necessary nor sufficient
for staleness to have occurred.

Implementation notes
--------------------
kyberforge ships a SessionStart hook (startup matcher only) that runs
`apm outdated`, and when anything is behind runs `apm update --yes` and returns
reloadSkills:true so the running session picks up redeployed content. It exits
silently with no apm.lock.yaml present, which keeps it inert for hosts that
installed this plugin natively rather than through apm.

Two findings drove the wiring, both verified rather than assumed:

- apm resolves ${CLAUDE_PLUGIN_ROOT} against the installed package root, and
  `apm pack` keeps only *.json from .apm/hooks/. A .../hooks/<script> reference
  therefore points into the generated mirror where the script does not exist —
  apm reports "Hook script not found" and deploys a hook aimed at nothing. The
  reference must be .apm/-relative, and a test pins it.
- apm's executable-trust gate is OFF unless apm.yml carries an `executables:`
  block; until now every hook, bin and MCP primitive a dependency shipped would
  have deployed unprompted. Root apm.yml now enables it. The allow key is
  version-pinned by apm's design, so a kyberforge version bump silently blocks
  the hook until the key is bumped too — called out in the block and the ADR.

Also corrects ADR-0018 and AGENTS.md, which named `apm install` as the refresh
command. It is not: `apm install` deploys from apm.lock.yaml's pinned commit
and does not re-resolve refs. `apm update` does.

Impact
------
Session startup costs ~0.7s when current and ~10.4s when six packages are
behind. Auto-refresh rewrites apm.lock.yaml, so an unexplained modification to
it after opening a session is expected; the emitted notice says so.

.claude/settings.json stops being exactly {"hooks": {}} once the hook lands
there — the merged entry is apm's own output, and the rule that nothing
repo-authored goes in that file is unchanged. .claude/hooks/ and the
.claude/apm-hooks.json sidecar are gitignored install output.

The hook cannot install itself: dependencies resolve from the remote, so it
takes effect only after this merges and `apm update` runs once against the new
default branch.

scripts/git-hooks/ is now empty. install.sh's copy block is kept and
test-git-hooks-install.sh synthesizes its own fixture, so the mechanism stays
tested without requiring a dead hook to exist.

ADR: 0019
Refs: #78

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 17:46:38 +00:00
b8dc400365 fix: reap only our own jobs, and say why a vale probe missed
batch_run ended with a bare wait, which blocks on every background job the
calling shell has, not the ones it started. Harmless for all three current
callers, but a future caller that backgrounds anything of its own would have
batch_run block on it or consume its status. It now records each $! and reaps
exactly those PIDs.

The `wait "$pid" || true` there is load-bearing: unlike a bare wait, wait <pid>
returns the job's status, so without it a single failing job would abort the
set -e caller at the call site -- before run-tests.sh or sync-plugin-content.sh
could read their .status files and print a summary. Status semantics stay in
those files, exactly as before.

check-vale-style-sync.sh's glob probe discarded vale's exit code and output and
decided purely on a grep, so a failed exec, an OOM-killed vale or a full TMPDIR
was indistinguishable from a real glob defect -- both printed "its glob sections
do not cover a path" with no evidence. A flake seen once in this probe could not
be diagnosed afterwards for that reason. The probe now attaches vale's rc and
output: a genuine glob defect reads "vale exited 0 ... in 0 files", a killed vale
reads "vale exited 137; output: <empty>".

That flake was investigated and not reproduced -- 1680 probes across three
contention setups including an offline namespace, all clean -- so nothing is
changed speculatively. The misattribution is worth recording: it was reported
against tests/test-vale-wrap.sh, which never invokes this script; the assertion
belongs to check-vale-style-sync.sh and reaches a log through a different suite.

Also drops the last stale field roster from agent-author's scaffolder. Its
next-steps hint enumerated "(name, description, model, body only)" -- omitting
disallowedTools, and never accurate anyway, since the template marks only
description and the body FILL IN. Its --help carried the inverted form, already
missing six forbidden fields. Both now state the shape rule and point at
field-inventory.md, and a bats case enforces all-or-nothing: name every
allowlisted field or name none, since a partial roster is the shape that goes
stale silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 14:21:04 +00:00
f4bb1cf4e5 docs: finish reconciling the agent write fence across the toolchain
The previous round taught agent-audit's validator to permit disallowedTools but
left the skill that writes agents still forbidding it, in six places. Running
agent-author on any of the three fenced orchestrators would have stripped the
fence, and nothing would have caught it: the validator's allowlist is a permit
list, so an absent field passes. The template was the worst of them, since its
comment is copied verbatim into every new plugin-scope agent.

Where a list had to be restated it is now a pointer to field-inventory.md's
apm-agent-allowlist instead -- the same data validate.sh reads -- because a
roster copied into a template goes stale one step further out than the roster
itself. Where the text has to teach something it teaches the shape rule rather
than the exception: tools is an allowlist whose vocabulary differs per harness,
so verbatim copy makes one value wrong on one target; disallowedTools is a
denylist, where an unrecognised name denies nothing, so the worst case is a
missing fence rather than a wrongly granted capability.

ADR-0016's amendment claimed an unrecognised key is inert on Copilot while the
same ADR's Context says that behaviour is unconfirmed by research -- asserting
as settled the exact thing it flags as unknown, and justifying it with apm's
compile-time behaviour, which says nothing about Copilot's runtime. It is
rewritten into labelled tiers: confirmed for Claude Code with citations,
inferred by analogy for Copilot with the analogy's limits stated, unverified
where it is unverified, and the residual risk accepted explicitly with its
blast radius. It also no longer claims to restore a write sandbox: the denylist
does not deny Bash, which these agents inherit and legitimately need.

docs/hooks.md called the old root hooks.json a stale sync artifact -- it was
added in the plugin's creating commit and pointed at by main's Copilot manifest
-- and claimed both ecosystems now resolve hooks/hooks.json. Copilot does not:
its hooks field has no default and no compiled manifest declares one, so it
resolves nothing. Recorded as the gap it is, with re-injection noted as a
follow-up rather than asserted away. Its event list is marked partial.

Also: new-agent.bats asserted a hardcoded four-field allowlist and would have
rejected a scaffolded agent carrying the field the ADR now blesses; it reads
field-inventory.md too. And ADR-0016's premise that Claude's tools: is
space-separated was wrong -- it takes a comma-separated string or a YAML list.
The incompatibility with Copilot is the vocabulary, not the punctuation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 12:30:30 +00:00
430f46b8e8 docs: correct the claims this review found false
AGENTS.md told an offline agent to push with SKIP=apm-marketplace-check and
asserted that hook was "the only one whose failure mode is 'no network'".
Running all 12 pre-push hooks under a network namespace shows two fail, for
one shared cause: apm-pack-check-clean resolves the same remote entry. An
exact pin does not remove the ls-remote, so both hooks are named now.

AGENTS.md also said everything in a plugin root except .apm/ is generated.
Plugin roots carry hand-authored README.md, docs/, bin/, sources.md and
.mcp.json, so an agent would hunt for an .apm/ source that does not exist or
refuse the edit. The rule is positional: immunity belongs to the plugin root,
and anything inside a mirrored directory is still rm -rf'd.

ADR-0017 said apm strips a hooks field. The real loop is (agents, skills,
commands, instructions) -- hooks absent, instructions never mentioned -- and
it can never fire, because synthesize_plugin_json_from_apm_yml only emits the
eight identity fields. The decision stands; the mechanism was overstated. Its
mcpServers amendment is rewritten for the pointer payload and now records the
real reason: inlining bypassed apm's credential sanitizer.

ADR-0015's owner.email and version-pin passages are corrected against the apm
source, and ADR-0016 gains the disallowedTools amendment. agent-audit's
allowlist is data, so it gains disallowedTools too -- the ADR and the
validator that enforces it had come apart.

architecture.md described a root CLAUDE.md that imports two files (it imports
one, plus an RTK block) and pointed at an ADR index that does not exist.
Seven skill READMEs listed tests/ files the mirror strips, promising installed
users files their install lacks; those rows are marked source-only, with the
depth-4 template tests explicitly called out as surviving. And
plugins/kyberforge/hooks/README.md, deleted during the conversion and
preserved nowhere, is restored to a path the mirror does not own -- verified
by running a sync against a scratch copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 11:04:56 +00:00
7ba3d9cf1d fix(apm): pin the remote package and restore the agent write fence
mattpocock-skills was pinned as the range ^1.2.0 with no lockfile, so apm
re-resolved it against upstream on every pack. An upstream v1.2.4 would
invalidate the committed ref/sha and fail apm-pack-check-clean with exit 4,
blocking every push in the repo at an unrelated moment, triggered by a third
party. ADR-0015 claimed the opposite -- that nothing advances it. Pinned to
1.2.3, which resolves to the already-committed sha, so the only compiled
change is the version key the remote entry alone was missing.

marketplace.owner.email was dropped on a false premise: ADR-0015 said apm has
no key for it, but yml_schema.py defines _AUTHOR_OBJECT_KEYS as {name, email,
url} and the key compiles through. Restored. (displayName is genuinely
unsupported and stays dropped.)

ADR-0016 dropped per-agent tools: because the allowlist shape is unportable --
Claude takes a comma list, Copilot a {Tool: true} map. That holds. But a
denylist has no such conflict: disallowedTools is honoured by Claude Code and
is absent from its plugin-subagent ignore list, and Copilot copies agent
frontmatter verbatim so an unknown key is inert. gitea-orchestrate,
apm-orchestrate and lint-runner were all write-denied on main and lost that
fence silently; only lint-runner's loss was disclosed, and only lint-runner
had prose to fall back on. All three regain the fence, and the two with no
no-edit language gain three statements each. git-orchestrate is untouched --
it legitimately had edit.

Four plugins shipped changed compiled output under unchanged versions,
against the policy this PR itself wrote: bin 1.1.1->1.1.2, git 1.3.2->1.3.3,
gitea 1.3.3->1.3.4, lint 1.1.5->1.1.6, each in both the plugin manifest and
the root packages[] entry. Root catalog 0.3.3->0.3.4: patch, because the set
of entries is unchanged and what moved is the owner block and four versions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 11:04:38 +00:00
013b913bd4 docs: correct claims the apm conversion left false
Documentation asserted things the code no longer did, which is how several of this
round's defects survived three review passes.

- `docs/spec/architecture.md` still described the pre-APM model — "each plugin has a
  `plugin.json` manifest" — when no such file exists. AGENTS.md routes agents here
  for the current structure, so it was live drift, not archive. Also cited ADR-0012
  where ADR-0003 is meant.
- AGENTS.md never said `.apm/` is the sole hand-edited source, while the generated
  mirror is byte-identical with no marker — an agent reading only AGENTS.md would
  edit the mirror and find out at pre-push. It also omitted that `apm` and `jq` are
  now required to push, unlike the `vale` bullet that exists for exactly that reason,
  and understated the pre-push gate. `apm marketplace check` hard-fails offline and
  `--offline` does not help, so the `SKIP=` escape hatch is documented rather than
  new machinery built.
- ADR-0017 carried hardcoded line numbers that drifted twice within one PR, and said
  `mcpServers` reinjection was real-syncs-only where the script's own header says
  both modes. Line numbers dropped rather than corrected.
- ADR-0015 asserted issue #90 was closed. It is open. Reworded to defer to the
  issue's own state rather than depend on someone remembering to close it.
- ADR-0014 said both audit skills pass `--config`; both SKILL.md files say pass none,
  and passing one fails with E100. The ADR was wrong.
- ADR-0007 cross-referenced an archived ADR-0011 whose number was later reused.
  Disambiguated with a note — renumbering a published ADR rewrites an immutable
  record and breaks every existing citation.
- CONTEXT.md claimed `lint-runner` is report-only via a missing `Edit` tool.
  Plugin-scope APM agents cannot express `tools:` (ADR-0016), so it is report-only by
  instruction now; ADR-0016 accepted that regression but CONTEXT.md was not updated.
- Recorded two undocumented consequences of the conversion: `displayName` dropped
  from all six compiled manifests and `owner.email` from the marketplace, and
  `mattpocock-skills` silently version-pinned and manually maintained.
- Wrote the version-bump policy issue #90 specified but never delivered. Its previous
  carrier, `marketplace-author`, was deleted in this PR, leaving the per-package rule
  documented nowhere.
- README fixes: kyberforge listed skills belonging to the `git` plugin and a deleted
  hooks path, `bin` advertised an agents directory it never had, and the docs index
  listed files that do not exist.

Refs: #90

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 01:53:26 +00:00
7910b8b12c docs(kyberforge): fix path drift and content gaps from apm conversion
The apm conversion (5e23250) moved skills/agents to plugins/<name>/.apm/
and deleted plugin-author/marketplace-author, but a review against issue
#90's Definition of Done found several stale pre-conversion references
left behind by straight git-mv's, plus one real content gap:

- ADR-0014 still documented the old flat vale-prefilter paths in 6
  places, despite ADR-0015 claiming it had been updated.
- ADR-0015 also overclaimed: it said ADR-0014 had both a skills/ and an
  agents/ path regex updated, but ADR-0014 never had an agents/ path
  regex to begin with (its one "agents" mention is a Vale glob-section
  identifier, not a filesystem path). Corrected the wording.
- CONTEXT.md's Skill glossary entry, its Vale-prefilter section, and its
  plugin-author/marketplace-author forward-pointer all still described
  the pre-conversion layout or a "pending issue #90" state that has since
  landed.
- LESSONS.md and two skill test READMEs pointed at skill-author/
  skill-audit/agent-author paths without the .apm/ segment.
- apm-workflow/references/marketplace.md cited plugin-author/SKILL.md's
  Gotchas for the Claude Code reserved plugin-name-prefix list, but that
  list was never actually carried into apm-workflow during the
  conversion despite ADR-0015 claiming it was. Recovered the list from
  git history and inlined it directly rather than leaving a dangling
  citation.
- agent-author/references/deployment-modes.md had an example
  contradicting its own stated .apm/agents/ convention two lines above.

Refs: #90
2026-08-13 07:50:56 +00:00
5e232503c4 feat(kyberforge): execute plugin-to-apm marketplace conversion
Why:
ADR-0015 established that Microsoft APM (apm.yml + .apm/) should replace
this repo's hand-authored plugin.json/marketplace.json model, with those
files becoming compiled output of `apm pack` instead of files edited by
hand via the (now-retired) plugin-author/marketplace-author skills.
Issue #90 was the deferred execution of that decision, gated on #88
(apm tooling) and #89 (apm-native agent-author/skill-author routing).

Implementation notes:
- All six plugins (bin, core, git, gitea, kyberforge, lint) now carry
  apm.yml + .apm/{skills,agents,hooks} as their authoring source. Skills
  moved with a plain git mv (content-identical across targets). Agents
  were re-authored, not moved: per ADR-0016, .apm/agents/*.agent.md
  compiles verbatim to both Claude and Copilot, so plugin-scope agents
  now carry only name/description/model/source_keys -- no tools: field,
  no Claude-only knobs (isolation, maxTurns, effort, memory,
  permissionMode).
- Root apm.yml registers all 7 marketplace packages (6 local plus
  mattpocock-skills as a remote entry) under versioning: per_package,
  matching this repo's existing independent-plugin-versioning practice.
- .claude-plugin/marketplace.json and every plugin's plugin.json are now
  apm-pack-compiled output, verified against the prior hand-maintained
  content: same names/descriptions/versions/licenses/authors, only
  cosmetic serialization differences (JSON key order, owner email vs.
  url, Unicode escaping).
- plugin-author and marketplace-author are retired now that apm-based
  authoring fully replaces their job; kyberforge bumped 1.3.1 -> 1.4.0
  for that removal, and the root marketplace catalog bumped
  0.3.1 -> 0.3.2 to match, per the version-bump convention now
  documented in apm-workflow's reference docs instead of a dedicated
  script (apm has no native version-bump automation).
- Fixed hardcoded pre-.apm/ path assumptions across
  .pre-commit-config.yaml, .pre-commit-hooks.yaml,
  scripts/check-scope-walkup-sync.sh, scripts/sync-vale-styles.sh,
  scripts/check-vale-style-sync.sh, six plugins' root plugin.json
  (stale skills/hooks/agents pointer fields that check-manifests.sh
  validates), and several tests/*.bats and tests/*.sh fixtures --
  including a bats REPO_ROOT relative-path depth bug (10 files, one
  extra .apm/ directory level to walk up) and a vale probe-path
  isolation regression introduced mid-fix.
- Corrected empirically-wrong assumptions surfaced this session in
  apm-workflow/apm-install's own reference docs: `apm marketplace
  package add` does not accept local paths (only owner/repo remote
  shorthand -- local packages are registered by editing apm.yml's
  marketplace.packages[] directly); `apm compile` is a consumer-side
  AGENTS.md/CLAUDE.md generator, not the plugin.json producer, and
  hard-fails on skill/agent-only packages without --clean; `apm plugin
  init <name>` nests a stray subdirectory when run with a positional
  name arg from inside a same-named directory; no native Copilot
  marketplace output profile exists; .mcp.json is merged into the
  compiled plugin.json content-aware and target-scoped, with no
  dependencies.mcp entry needed for simple passthrough; pipx is the
  correct pip fallback on externally-managed Python environments.
- Renamed agent-author's copilot.agent.md template asset to
  copilot.agent.md.template so apm compile's recursive *.agent.md glob
  stops misparsing the placeholder template as a real agent primitive.

Impact:
plugin.json and marketplace.json are compiled artifacts from here on --
editing them by hand is no longer the workflow; edit apm.yml/.apm/ and
run apm pack. CONTEXT.md's Plugin/Plugin marketplace glossary entries
reflect this. ADR-0001 is marked superseded, ADR-0006 moot, and
ADR-0010 updated for the new .apm/agents/ path (project/user scope
unaffected, per ADR-0016). Full local verification: claude plugin
validate --strict on all 6 plugins, apm audit --ci, apm marketplace
check, check-manifests.sh, and the full test suite (165/165 bats,
13/13 shell scripts) all pass clean.

Fixes: #90
Refs: #88, #89
ADR: 0015
ADR: 0016

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ub96PyaSRD9BHPktotj1pC
2026-08-12 18:21:24 +00:00