c52e351954270ebc0eaedfca7f992605c8b771d4
569
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
aa15fc850c |
test: fail the gate when a suite is skipped or never reports
Three gaps left by the previous round, all the same shape: a gate reporting success having verified less than it appears to. run_bats() hard-failed on a missing or non-executable runner but never checked that the runner produced anything. An empty, executable run-bats.sh exits 0, and the dispatcher printed a green summary with 166 bats tests silently absent. It now requires an "N tests, M failures" line with a non-zero count. run-tests.sh's skip listing is swallowed by pre-commit on a pass, so on a machine without vale three suites exited 77 and the pre-push gate went green having run 14 of 17. The hook now invokes it as --strict, where a skip fails and the error names each suite and the reason it skipped. An ad-hoc local run still skips gracefully -- at pre-push a skip means a documented dependency is missing, which is a setup error, not a legitimate state. Deliberately not wired to the vale downgrade's env var: one flag must not disarm two gates. BATS_FILE_FLOOR is replaced by an expectation derived from git ls-files. A floor of 8 against a real count of 10 let two files and eleven tests disappear green, and the number needed an edit whenever a plugin was added. The derived set needs no number, and catches an addition as well as a removal -- a .bats file staged into the index and deleted from disk is now demanded back. The vale opt-out announced its downgrade to nobody: pre-commit prints nothing for a passing hook, so the summary line AGENTS.md tells the reader to check was unreachable in exactly the situation it exists for. The hook is now verbose. Also corrects the PROBES_CHECKED guard, whose commit message described a state that cannot occur -- the .vale.ini loop errs first. Its two reachable triggers, a gutted probe heredoc and a probe row naming a missing directory, had no test; they do now, each asserting the guard is the sole cause. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
874bf06b18 |
chore(apm): regenerate compiled manifests and content mirrors
Output of apm pack, sync-plugin-content.sh --all and sync-marketplace-mirror.sh against this round's source changes. No file here is hand-edited. Carries the version bumps and marketplace owner.email into the compiled manifests, the disallowedTools frontmatter and doc corrections into the flat mirrors, and changes plugins/bin/.github/plugin/plugin.json's mcpServers from the inlined server object to the ".mcp.json" pointer. That last file also returns to 0644: the previous re-injection wrote it through mktemp and carried 0600 across, which no gate could see because the mode check did not cover .github/plugin/ and git tracks only the exec bit. .agents/plugins/marketplace.json is unchanged and that is correct -- apm's codex profile carries neither version nor owner keys, so nothing in this round reaches it. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
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 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
|
||
|
|
52bbd62286 |
test: stop the runners reporting green on suites that never ran
run_bats() was an if with no else, so a missing or non-executable run-bats.sh made all 166 bats tests disappear with no diagnostic while the run printed a green summary and exited 0. --bats-only became a total no-op. This is the same defect the PR fixed one level down, left open in the dispatcher that pre-push actually invokes -- and nothing tested run-tests.sh at all. run-bats.sh's aggregation was asserted by nothing. Three separate mutations to its failure-detection line all survived the existing suite, because real bats emits both a nonzero exit and "not ok" lines, so each signal masked the other. The new cases produce each signal without the other; all three mutants now die. Also in this pass: - zero discovered .bats files exited 0, so a widened path exclusion retired the suite silently. Replaced with a file-count floor: a collapse to one or two files is the same failure as a collapse to zero - an existing-but-empty status file counted as a pass, because [[ "" -eq 0 ]] is arithmetic-true and the || echo 1 fallback only covered a missing file. The repro is deterministic: the stub truncates its own status file, then kill -9s its parent so the real exit-code write never happens Adds tests/test-run-tests.sh, 9 cases pinning the exit 0 / 1 / 77 three-way split against a single summary line. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
af085ed057 |
fix(scripts): fail the vale and scope gates when they cannot verify
check-vale-style-sync.sh's glob-coverage probe silently self-disabled when vale was absent from PATH, exiting 0 on the one-character glob typo it exists to catch. pre-commit swallows a passing hook's output, so the pre-push hook reported Passed. The script already hard-fails on a bad REPO_ROOT for exactly this reason -- "a clean exit 0 here would read as 'checked, in sync' when nothing ran at all" -- and six of its assertions are vale invocations. Absence now fails; the opt-out is an env var that must be set deliberately, and it downgrades the run to text-level assertions while saying so in the summary. Neither script had a floor on its rewritten .apm/ paths, so relocating .apm/ made both exit 0 -- and this PR's whole change to them was a path rewrite, the exact edit that failure mode survives. A third gap the directory check could not see: relocating only assets/vale/ left both audit skill directories in place while every probe continued past its missing .vale.ini, skipping the whole table with FAIL=0. A zero-probe run is now an error. Both test suites encoded the vacuous pass as a passing case. Those cases are now scoped to "no plugins/kyberforge at all" and assert the fixture really lacks it, with new counterparts covering the drift shape and new positive cases requiring each script to report a non-zero inspected-target count. Also removes the HOOK_REGEX_CACHE memoization: every call site was a command substitution, so the writes happened in a subshell and the lookup always missed. Measured at 14ms of an ~870ms run, all of which is the six vale invocations. Deleted rather than repaired -- 35 lines claiming a benefit they never delivered is worse than no cache -- with a comment recording why. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
3f1ee47f1e |
fix(scripts): stop check-manifests passing on entries it cannot parse
A marketplace entry missing its source key disabled both directions of the check at once. The helper required source to be a string, so a source-less entry was skipped and its plugin.json existence check never ran; the name axis selected on (.source | type) != "string", and null != "string" is true, so the same entry also marked its on-disk directory as listed. Delete source from an entry and delete its plugin.json and the script exited 0. Because sync-plugin-content.sh --all derives its work list from the same helper, that plugin silently dropped out of the content-mirror gate too. Also in this pass: - a wrongly typed skills value crashed the script mid-loop with a raw jq error and no "Manifest check failed:" line, leaving every later plugin unchecked. Note skills is legally string|string[] per both host schemas, so a string now resolves as a single path rather than erroring - array- and object-valued pointer fields were reported missing even when they resolved, because the whole JSON value was pretty-printed into a path test - an unparseable marketplace.json died inside a process substitution, so the run reported six "no entry in marketplace.json" errors that sent the reader to edit apm.yml when the real fault was a corrupt manifest - a missing marketplace.json exited 0 even with plugin directories present Tests: 14 -> 23 assertions. Every failure case asserts on message text, not exit code alone, since exit 1 here is reachable by several causes that call for opposite fixes. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
9e612fd183 |
fix(scripts): re-inject .mcp.json as a pointer, not resolved content
reinject_mcp_servers copied .mcp.json's mcpServers into the compiled Copilot
manifest verbatim via jq. apm's own path does not: collect_mcp_servers runs
_sanitize_mcp_servers(), which drops env/headers/authorization and redacts
secret-shaped keys, because copying them into a committed manifest exfiltrates
them into the distributed artefact. The re-injection was the only route around
that sanitizer, and it wrote to a tracked, marketplace-distributed file.
Both host schemas document mcpServers as "string or object -- config path or
inline definitions", so the pointer form is valid and carries no resolved
content. It also preserves the ${VAR} indirection the sanitizer strips.
Also in this pass:
- mktemp+mv left the manifest at 0600 while --check compared content only, so
a real sync silently demoted a mode the gate could not see
- --check --all exited 0 when the marketplace yielded zero plugins, including
on unparseable JSON: the one gate whose work list comes from a generated file
could be silenced by regenerating its own input
- sync_dir took an unguarded $target_dir despite a comment claiming otherwise
- basename '.'/'..' escaped $SCRATCH_ROOT and made bundle selection arbitrary
- path_manifest compared only the exec bit, so check and sync disagreed
- sync-marketplace-mirror.sh fell back to pwd outside a worktree and reported
no drift on a tree it never identified
Mode comparison is deliberately files-only: directory modes come from umask on
one side and checkout on the other and git tracks neither, so comparing them
reports the runner's umask rather than a property of the mirror.
Tests: 44 -> 67 and 15 -> 19 assertions, each verified to fail under the
mutation it exists to catch.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
|
||
|
|
0f0ac5821f |
docs: reconcile AGENTS.md's pre-push hook count with what the command reports
AGENTS.md said 12 pre-push hooks and recommended a command that reports 14, so a reader following the instruction hit a mismatch on the first try. The repo defines 12; pre-commit's own `meta` hooks, check-hooks-apply and check-useless-excludes, declare no `stages:` and therefore also run at pre-push. Refs #97 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
c442f7eb85 |
fix(scripts): decide .vale.ini readability by reading it, not by access(2)
Issue #97 item 1 reports the unreadable-.vale.ini guard as untested. It was worse: it was dead. `[[ -r ]]` is access(2), which asks whether the permission bits would allow a read -- and for uid 0 that is yes even on a mode-000 file. This hook runs at pre-push and the dev environment is root, so the guard could never fire where it exists to fire. That is why no uid-independent test for it existed; there was nothing to test. Readability is now decided by actually reading (`cat`), which is uid-independent and strictly stronger, catching EISDIR and EIO that access(2) reports on neither. `cat`, not a `< "$ini"` redirect: opening a directory for reading succeeds, only the read fails. The missing branch moves to `-e`, so a directory sitting where the file belongs is reported as unreadable rather than sending the reader hunting for a deleted file. The new case asserts the MESSAGE, not the exit code. With the guard removed the script still exits 1 -- the greps hit the unreadable path and blame a missing StylesPath on a file that has one. An exit-code-only test would have been green with the guard deleted. Also stops paying for vale in cases that only assert .vale.ini text: 21 of 28 script runs now mask it via the PATH_NO_VALE mechanism case 12 already builds, cutting the suite's bottleneck ~3.5x (issue #97 item 5). The helper falls back to an unmasked run rather than skipping, so a machine where masking is unavailable loses speed, never coverage. That masking is a coverage gain, not only a speedup. With vale on PATH, cases 8 and 9 could not detect deletion of the assertions they were written to catch: a dropped StylesPath also breaks the glob probe, so the script exited 1 for the wrong reason and both cases went green. Verified against the pre-change files -- the same mutation was caught by one incidental assertion before, and by three after. Refs #97 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
5a61b417c9 |
fix(scripts): bring the generated hooks/ directory under the mirror's ownership
`checked_paths` covered hooks/hooks.json but not the hooks/ directory holding it, so a stray file dropped inside, or an empty hooks/ left behind once .apm/hooks/ stopped producing anything, was invisible to --check. Check and sync agreed in both cases, so the invariant held -- but a stray in a directory the mirror owns should be drift, exactly as it is inside skills/ or agents/. A stray at the PLUGIN root stays out of scope by design: README.md, docs/, bin/, .mcp.json are hand-authored. hooks/ is now wiped and rebuilt like every MIRROR_DIRS destination, and the directory is listed in checked_paths so the recursive manifest sees one-sided entries. Issue #97 item 4 reports `prompts` as documented-but-unmirrored. That is refuted: MIRROR_DIRS lists DESTINATION directories, and apm folds .apm/prompts/ into commands/ (renaming *.prompt.md to *.md), verified empirically. A plugin adding .apm/prompts/ is mirrored today; adding a `prompts` entry would name an output directory apm never emits. Pinned with a characterization test that fires if that mapping ever changes, plus a comment so it is not refiled. Guards the new wipe with ${target_dir:?}: `set -u` aborts on an unset variable but not an empty one, which would make it `rm -rf /hooks`. Refs #97 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
73393b9d01 |
fix(scripts): correct shellcheck source directives that resolved to nothing
`tests/run-tests.sh` declared `source=lib/batch-run.sh`, which resolves to neither the repo root nor the script's own directory. A directive that does not resolve is silent: it blinds test-vale-wrap.sh's `sourced_files()` seeding exemption, and shellcheck's own SC1091 is `info` while .pre-commit-config.yaml pins `--severity=warning`. Issue #97 names run-bats.sh's `../scripts/lib/batch-run.sh` as the correct spelling. It is not. Directives resolve against the source-path, which under pre-commit is the repo root, so `../scripts/...` escapes the repo and trips SC1091 exactly as `lib/...` does -- verified directly. The spelling satisfying both shellcheck and `sourced_files()`'s two-candidate rule is repo-root-relative, matching scripts/install.sh. Fixes all three: run-tests.sh, run-bats.sh, and check-manifests.sh, the last unmentioned by the issue. Every directive in the repo now resolves, which the previous commit's case 27 asserts. Refs #97 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
49d21bcb4d |
fix(providers): guard the statusline's unguarded array expansion
`parts` is seeded empty and all seven appends are conditional, so
"${parts[@]}" at the join loop can expand an empty array. install.sh
deploys this file to every user machine.
Two things had to both hold for the bare form to be safe: this file
enabling no `set -u`, and the shell being bash 4.4+, which stopped
treating an empty-array expansion as unbound. On bash 3.2 -- macOS's
system bash, an explicit repo target -- adding `set -u` aborts here.
That is also why the hazard is unreproducible on a modern dev box and
why the enforcement is a static scan rather than a runtime test.
Adds the `providers` glob to test-vale-wrap.sh's bash-3.2 scan, which
excluded it precisely because of this defect. Floor is 1 rather than
"count minus slack": the glob holds one file, so any slack at all
means a floor of 0, which passes vacuously on a renamed directory.
Also adds case 27, the regression test for the stale `shellcheck
source=` directives fixed in the next commit (#97 item 2). It lives in
this file because that is where the exemption it guards lives.
Closes #96
Refs #97
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
|
||
|
|
55d956b298 |
docs: log two lessons from PR #95's fourth review round
The first is new: four fixes specified by the orchestrating reviewer were wrong, each in a way that would have shipped a guard catching nothing. A finding arrives with evidence and gets scrutinised; the fix beside it arrives with the same authority and gets implemented. Worth naming because the failure is silent — the guard looks right and the suite stays green. The second is flagged as a graduation candidate rather than promoted: it overlaps two 2026-08-09 entries on assertions that pin something adjacent to what they name. Per the graduation rule, promotion to core/instructions/testing.md is the human's call, not mine. Refs: #95 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
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 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
bb9158da22 |
chore(apm): correct core's published metadata and regenerate compiled output
`core`'s description and keywords described `bin`'s skills — triage, diagnosis, architecture review — while `core` ships the three agentsmd/provider-adapter skills. Pre-existing text, but this PR compiles it into `plugin.json` and both `marketplace.json` files, so it became published metadata. The root `apm.yml` entry carried the same stale text and is the one that actually reaches the marketplace manifests, so fixing only the package manifest left it half-propagated. Version bumps follow the per_package policy this PR documents, each landing in both the package `apm.yml` and the root entry: kyberforge 1.4.0 -> 1.4.1 for the reference-doc changes, core 1.1.0 -> 1.1.1 for the metadata, catalog 0.3.2 -> 0.3.3 as a marketplace-block-only edit. The compiled diff is larger than those four changes because the manifests committed at HEAD were not apm output: `json.dumps(indent=2, sort_keys=True)` reproduces all three byte-for-byte, i.e. `pretty-format-json --autofix` had rewritten them before the exclusion existed. Regenerating realigns them to genuine `apm pack` output; the remainder of the diff is key ordering and UTF-8 escaping. `core`'s keyword change is a fifth semantic change, invisible at marketplace level because `marketplace.json` carries no keywords field. Refs: #90 ADR: 0015 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
413a750819 |
fix(scripts): close gates that passed while the thing they guard was disabled
Four repo gates reported success in states they exist to reject. `check-vale-style-sync.sh` passed while a Kyberforge lint rule was silenced. The check matched a blocklist of severity values, but Vale's semantic is an allowlist: anything that is not exactly YES/error/warning/suggestion disables the rule. So `= false`, `= 0`, `= garbage`, an empty value and — worst — a lowercase `= yes` all killed enforcement while reading as "enabled" to a human. Inverted to an allowlist. Two sibling holes: dropping `KyberforgeCopilot` from `BasedOnStyles` unloaded the Copilot-only check silently, and narrowing a section glob to a location made Vale lint zero files, which is the "0 files, hook Passed" failure the script's own comment says it exists to catch. `sync-marketplace-mirror.sh --check` failed open when its source was missing, while its sibling correctly errored in the same state. `check-scope-walkup-sync.sh` wrote to hardcoded `/tmp/fN.out` paths and read one back, making it non-reentrant — a concurrent instance can flip a verdict, and this branch made the test runner concurrent. Now per-run `mktemp -d`. `check-manifests.sh` had no disk-to-marketplace pass, so a plugin directory absent from `marketplace.json` passed every gate while the `validate-plugins` hook globbed it. The "listed" match is restricted to remote-source entry names; matching any entry name let a genuine orphan through on a name coincidence. `run-bats.sh` reported an empty TAP stream as `0 tests, 0 failures`, exit 0 — a total harness failure reading as a pass. The test-side changes are the larger half, because the guards were the real problem. `test-sync-marketplace-mirror.sh` could overwrite the live tracked mirror under an inherited GIT_DIR, which is precisely the git-hook context it runs in. The bash-3.2 scan hand-maintained its file list, omitting the new shared runner, and had no rule for `wait -n` or `nproc` — the two hazards the previous review round found live. It now derives 43 files across three globs with per-glob floors. Several assertions were decoration: the concurrency checks caught the reentrancy defect 0 times in 10, the leak fix was green either way, and two manifest fixtures passed with the code they claimed to cover deleted. Every assertion now has a revert it provably fails against. Refs: #90 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
d4fa4b7153 |
fix(kyberforge): stop the content mirror amputating a shipped template asset
The mirror's `tests/` exclusion was depth-agnostic, so it deleted `skill-author/assets/templates/tests/` — a template the skill scaffolds FROM — alongside the depth-2 dev fixtures it was meant to drop. Since ADR-0017 makes the mirror the installed content, the shipped scaffolder was broken: the mirror copy of `new-skill.sh` exited 2 on `sed: can't read .../tests/README.md`, leaving a half-written skill, while the byte-identical `.apm/` copy exited 0. `--check` was green about it. Check mode was restructured rather than patched because `diff -x` matches a basename at any depth and cannot express the depth-2 scoping the fix needs — the two modes could not be made to agree by construction. Check mode now runs the real `sync_dir` into a throwaway root and diffs with no exclusions, leaving the exclusion rule and the hooks destination each in exactly one place. Also fixed here, all previously invisible to `--check`: - Merged hooks were written to `<plugin>/hooks.json`, which Claude Code does not convention-scan, while ADR-0017 itself quoted `hooks/hooks.json` as the contract. Moved, with the legacy path cleaned up as stale. No `hooks` pointer is added to `plugin.json`, so this does not reopen the option ADR-0017 rejected. - Only the first drift per plugin was reported: `diff | sed` returns 1 under `pipefail`, and `set -e` killed the subshell before the remaining checks and before `FAIL=1`. - File-mode and symlink drift were invisible, so `--check` and a real sync disagreed; a find-based type/mode manifest now covers both. The tests pinned almost none of this — the stale-skill wipe, the check-mode stale branch, three `MIRROR_DIRS` entries and the hooks newline normalization could each be deleted with the suite still green. All are now mutation-tested. Refs: #90 ADR: 0017 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
f6cf83c841 |
fix(ci): make the content-sync gate fire on the cases it exists to catch
`check-plugin-content-sync` was the only local pre-push hook scoped with `files:` instead of `always_run`, and its regex missed both compiled `plugin.json` paths — exactly the manifests the script had been taught to diff. A commit tampering only with one of those passed the entire pre-push suite at exit 0. A regex fix alone could not close it: pre-commit drops deleted paths from the file list, so removing a mirror directory skipped the hook too. The `hooks\.json` alternative was also already dead, matching only the pre-move flat path. Dropping `files:` for `always_run: true` closes all three and makes the hook uniform with its three siblings; the check costs ~2-4s. Also excludes `.github/plugin/marketplace.json` from `pretty-format-json`. This was not hypothetical: `json.dumps(indent=2, sort_keys=True)` reproduces the manifests committed at HEAD byte-for-byte, so `--autofix` had already rewritten compiled apm output once. Without the exclusion it would do so again on the next commit, silently breaking the byte-identical mirror invariant. Landing first, ahead of the regenerated manifests, so that cannot happen. Refs: #90 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
e79497b3cf |
fix(tests): guard remaining bash 3.2 hazards from PR #95 review
Review findings #5 and #7 on PR #95 flagged two bash-3.2-incompatible patterns despite the surrounding scripts claiming 3.2 safety: - tests/run-bats.sh used `mapfile` (bash 4.0+), which fails immediately under macOS's stock bash 3.2 before any batching logic runs. Replaced with the `while read` loop already established in tests/run-tests.sh, and guarded the two downstream `${TEST_FILES[@]}` expansions with `${arr[@]+"${arr[@]}"}` to match that file's convention. - `trap 'rm -rf "${CLEANUP_DIRS[@]}"' EXIT` was unguarded in tests/test-sync-marketplace-mirror.sh and tests/test-sync-plugin-content.sh: under `set -u`, if `mktemp -d` fails before the array is populated, the trap itself throws an unbound-variable error that masks the real test failure. A repo-wide grep for the same pattern turned up a third, unreviewed instance in tests/test-check-release-needed.sh. Fixed all three with the guarded idiom already used elsewhere in the repo. Extended the existing bash-3.2-hazard static check (test 16 in tests/test-vale-wrap.sh) to scan all four fixed files going forward, so a regression of either pattern fails the suite instead of only surfacing on a real bash 3.2 host. Refs: PR #95 Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
23cef3627a |
fix(kyberforge): restore pointer-field validation for non-apm plugins
Skills/hooks/mcpServers/agents pointer-field validation in plugin.json was fully delegated to sync-plugin-content.sh --check, but that script explicitly skips any plugin directory lacking .apm/ (it has nothing to compile there). A plugin with no .apm/ and a hand-authored plugin.json whose pointer field points at a missing path was therefore left uncovered by either check -- currently latent since every plugin in this repo has .apm/, but a real gap for the first non-apm plugin added. Restores a fallback validation path here for exactly that case (no .apm/ directory), reusing the pre-delegation logic this script used to run unconditionally. apm-native plugins keep relying on the delegated check so the two never duplicate (or disagree) on the same manifest. Also switches the marketplace.json walk to the shared scripts/lib/marketplace-plugins.sh helper introduced alongside sync-plugin-content.sh's matching --all branch, replacing the near-identical hand-duplicated loop this script's own header comment already flagged as a duplication risk. Adds fixtures: a non-apm plugin with a broken skills pointer (caught), a non-apm plugin with a valid pointer (no false positive), and an apm-native plugin with a broken pointer (left to the delegated check, not double-validated here). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
fd70c8d65e |
fix(kyberforge): catch plugin.json and hooks.json drift in sync-plugin-content.sh --check
--check's throwaway pack copy seeded .claude-plugin/plugin.json and
.github/plugin/plugin.json from the real plugin dir, then packed without
--force -- apm pack silently skips regenerating a plugin.json that already
exists, so the diff always compared the copy against itself and never caught
drift in the compiled name/version/description/mcpServers. --force is now
always passed; in check mode it forces regeneration inside the throwaway copy
only, which sync_plugin_manifest() then diffs against the real committed
manifest.
sync_hooks_json() returned early whenever .apm/hooks/ was missing, without
checking whether a stale hooks.json was still sitting at the plugin root from
a prior sync -- unlike sync_dir(), which already detects that kind of orphaned
mirrored output. It now mirrors sync_dir()'s shape: flagged as drift in
--check, removed on a real sync.
Running the corrected --check --all against this repo's own plugins surfaced
3 real orphans: plugins/{git,gitea,core}/hooks.json, empty stubs added in
|
||
|
|
07ea0aeb17 |
fix(kyberforge): stop union-masking drift between vale-audit-prefilter manifests
hook_file_regexes() unioned the `files:` regex from .pre-commit-hooks.yaml and .pre-commit-config.yaml before checking whether a probe path is in scope of a kyberforge vale-audit-prefilter hook. That union let a probe matching only the old, looser .pre-commit-hooks.yaml pattern pass even after .pre-commit-config.yaml's copy of the same hook had been narrowed (e.g. to require a `.agent.md` suffix) -- silently masking exactly the kind of hook-rescoping drift this check exists to catch. Per ADR-0014 the two manifests are meant to exercise the same resolution path an external consumer's hook would, so this divergence is real drift, not noise. hook_file_regexes() now takes the manifest path explicitly and caches per (skill, manifest) pair instead of per skill, so each manifest's regex set can be inspected on its own. The probe-validation loop computes in_hooks/in_config independently via a new matches_any_regex() helper. Probes carry a new third heredoc field, `shared` or `hooks-only`: `shared` probes (a file shape genuinely covered by both manifests, e.g. plugins/demo/.apm/agents/demo.agent.md) must agree between the two or the check now fails with a drift error; `hooks-only` probes (a Copilot .agent.md living outside this repo's own plugins/.apm/ layout, and the legacy bare-`.md`-under-agents/ shape kept only to exercise a distinct .vale.ini glob section in isolation) are exempt, since .pre-commit-hooks.yaml is deliberately broader there by design. The original "matches no regex in either manifest" staleness check is unchanged. Added case 11b to tests/test-check-vale-style-sync.sh: narrows a fixture's local config regex further while leaving .pre-commit-hooks.yaml untouched, and asserts the check now flags it. Confirmed red against the pre-fix script before applying the fix. Refs: #95 |
||
|
|
925f04acdb |
fix(agentsmd-audit): scope secrets-scanner placeholder allowlist to matched token
validate-secrets.sh checked the placeholder allowlist regex against the whole line before running any secret-pattern regex. An unrelated placeholder-looking token anywhere on the line (e.g. "example" or "your-token-here" in a trailing comment) suppressed detection of a real credential earlier on the same line. Scope the allowlist check to the matched secret-candidate substring only, which the per-match re-check already did downstream but the whole-line pre-check short-circuited before it ever ran. Extend validate-secrets.bats with a case proving a real AWS-style key is still caught when a placeholder token sits elsewhere on the line. Regenerate the flat-mirror copy at plugins/core/skills/agentsmd-audit/scripts/validate-secrets.sh via scripts/sync-plugin-content.sh --all per ADR-0016. |
||
|
|
c6490096da |
fix(kyberforge): scope check-plugin-content-sync to sync-relevant paths
The pre-push hook ran a full `apm pack` + directory diff across every plugin on every push (always_run: true), even for changes that could never affect the sync (e.g. a README edit). Add a files: regex scoped to what scripts/sync-plugin-content.sh actually consumes -- each plugin's apm.yml, .mcp.json (read by reinject_mcp_servers), .apm/** (the sync source), the flat mirror output dirs themselves (agents/, skills/, commands/, instructions/, extensions/, hooks.json -- so a direct edit to compiled output is still caught as drift), the marketplace.json --all reads to build the plugin list, and the sync script itself -- and drop always_run now that files: covers every input path. |
||
|
|
911daddbe2 |
docs(kyberforge): fix stale references in CONTEXT.md
plugin-author was deleted by this PR (ADR-0015, apm-workflow is the successor) but still appeared as a live example in the skill-composition list. The Gitea-canonical-issue-tracker line also cited ADR-0017 (the unrelated plugin-content-mirror ADR added by this PR) instead of ADR-0007. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
b0b1470f2c |
fix(kyberforge): guard unguarded array expansions in sync-plugin-content.sh
force_flag and plugin_dirs expanded unguarded under set -euo pipefail,
tripping "unbound variable" on bash 3.2 (macOS) whenever the array is
empty -- which is the default --check invocation wired into the
pre-commit drift gate. Apply the same ${arr[@]+"${arr[@]}"} guard
already used for seen_names in this file.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
|
||
|
|
560154c727 |
docs(kyberforge): note repro caveat for ADR-0017 verification command
Running ADR-0017's cited live behavioral test literally from this repo's root gives a contaminated signal: this repo's own project-level .claude/settings.json enables all 6 holocron plugins, so Claude Code loads all of them rather than isolating kyberforge's discoverability. Documents the neutral-cwd + absolute --plugin-dir reproduction needed to isolate the signal. |
||
|
|
2c731eb476 |
test(kyberforge): cover sync-marketplace-mirror.sh drift-guard
Its sibling sync-plugin-content.sh has thorough coverage (tests/test-sync-plugin-content.sh) but this drift guard -- wired into pre-commit as check-marketplace-mirror-sync, keeping .claude-plugin/marketplace.json and .github/plugin/marketplace.json in sync -- had none. A silent regression here would let the two marketplace manifests drift without any test catching it. Covers: missing-source no-op in both real-sync and --check modes, drift detection when the mirror is missing or stale, real sync producing a byte-identical mirror, --check clean immediately after sync, drift from an edited source cleared by re-sync, an already-in-sync mirror reporting no drift, usage-error rejection of unrecognized/extra arguments, and idempotency of repeated syncs. |
||
|
|
7c3c867e00 |
docs(kyberforge): fix stale vale-prefilter regex in CONTEXT.md
The Vale-audit-prefilter glossary entry still quoted the pre-.apm/- migration file-scoping regexes (^plugins/[^/]+/skills/..., ^plugins/[^/]+/agents/...). The actual pre-commit hooks require .apm/ in the path and .agent.md for agent files; updated the glossary to quote the live patterns from .pre-commit-config.yaml verbatim, including a third stale occurrence in the skill-size-check description that the PR #95 review missed. Refs: #95 |
||
|
|
4003c6a273 |
fix(kyberforge): make bats dispatcher bash-3.2 safe
run-bats.sh's new bounded parallel dispatcher used nproc/wait -n, which are bash 4.3+/GNU-only and silently drop the concurrency cap on macOS's stock bash 3.2 (the wait -n error is swallowed by `|| true`). Its sibling tests/run-tests.sh, changed in the same PR and explicitly bash-3.2-safe, already solves this with getconf + a batched wait. Ported that same pattern here for consistency and to actually meet the compatibility goal. Refs: #95 |
||
|
|
9c140efa2e |
fix(kyberforge): harden plugin-content sync, reinject Copilot mcpServers
PR #95's review of the issue #90 apm-conversion work found several defects in scripts/sync-plugin-content.sh and the gate wired to it: - --check claimed never to mutate the plugin root, but apm pack still wrote .claude-plugin/plugin.json and .github/plugin/plugin.json into the real plugin_dir on first-time creation. --check now packs a throwaway copy instead. - check-plugin-content-sync hardcoded the six plugin directories instead of deriving them the way check-manifests.sh already does. Added an --all flag that parses .claude-plugin/marketplace.json, and simplified the pre-commit hook to use it. - A missing plugin_dir and one that legitimately has no .apm/ yet both reported SKIP/success; a missing directory now FAILs. - The dispatch loop backgrounded every plugin with no concurrency cap, unlike the JOBS-bounded pattern this same PR added to tests/run-bats.sh and tests/run-tests.sh. Added the same bash-3.2-safe getconf + batched-wait cap here for consistency. - Per-plugin scratch/log/status files were keyed only by basename, with no collision guard across arguments; added a fail-fast check. - sync_hooks_json()'s trailing-newline normalization was duplicated between its --check and write branches; factored into one helper. - tests/test-sync-plugin-content.sh set two competing `trap ... EXIT` statements, so the first (cleaning up $FIXTURE) was silently replaced by the second and its tmp dir leaked every run. Adopted the track()/CLEANUP_DIRS pattern already used in tests/test-check-release-needed.sh. Separately: apm's Copilot-ecosystem plugin.json builder unconditionally strips mcpServers, citing (in its own docstring) that the field is out of schema for Copilot -- a claim this repo's own researched Copilot plugin schema docs contradict. reinject_mcp_servers() narrowly restores it from the plugin's .mcp.json on real syncs only, regenerating plugins/bin/.github/plugin/plugin.json (the only plugin that currently declares any MCP servers). Documented as an amendment to ADR-0017, since it's a deliberate, narrow exception to that ADR's rejection of patching apm's compiled output -- apm's premise for stripping skills/agents/commands/hooks pointers is still accurate; its premise for stripping mcpServers is not. All 12 assertions in tests/test-sync-plugin-content.sh pass individually, plus 5 new regression tests added for this round; the full bats and shell-script suites are green; shellcheck is clean. Refs: #95 ADR: 0017 |
||
|
|
bff9662c52 |
docs(kyberforge): add keywords/homepage/repository to plugin apm.yml
bin and kyberforge had empty keywords: [], and no plugin set homepage or repository despite the schema supporting both and the repo having one canonical Gitea remote -- flagged by an apm-docs-vs-repo audit following issue #90's plugin->apm conversion. Compiled .claude-plugin/plugin.json and .github/plugin/plugin.json were regenerated via scripts/sync-plugin-content.sh to carry the new fields. Refs: #90 |
||
|
|
a873e93050 |
fix(kyberforge): keep .github/plugin/marketplace.json synced
apm.yml only declares claude and codex marketplace output profiles -- codex writes a differently-shaped file to .agents/plugins/marketplace.json, so nothing regenerates the legacy Copilot CLI path at .github/plugin/marketplace.json. It was hand-synced once during the #90 conversion and had drifted (missing the category field added when codex output was enabled) with no gate to catch it. scripts/sync-marketplace-mirror.sh keeps it byte-identical to the compiled .claude-plugin/marketplace.json, wired as a pre-push check so it can't go stale silently again. Refs: #90 |
||
|
|
a8beff7d2c |
perf(kyberforge): parallelize the bats and shell test runners
Both runners spawned one process at a time and let it finish before starting the next, so wall time scaled with file count even though each file's own work (bats: mostly repeated apm/git subprocess startup; the vale-heavy shell scripts: repeated vale binary startup) rarely used a full core. run-bats.sh now backgrounds one `bats` invocation per file, bounded by core count, buffering each file's TAP output so concurrent streams can't interleave, then flushes in stable sorted order once every job finishes. run-tests.sh does the same for test-*.sh in fixed-size batches (plain `wait`, not `wait -n`, to stay on the bash-3.2-safe path test-vale-wrap.sh already enforces for this file) -- verified beforehand that every test-*.sh keeps its fixtures under its own mktemp dir rather than mutating the live repo tree, so running them concurrently is safe. Confirmed correct on both the pass and fail paths (a deliberately failing bats test and a scratch TEST_DIR with pass/fail/skip-77 scripts) before measuring: full bats suite 26-30s -> 7.5s, tests/run-tests.sh 68s -> ~45-50s, full pre-push gate 2m40s -> 1m12s. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
c3a56d89f0 |
perf(kyberforge): scope skill-frontmatter to .apm/ and parallelize plugin sync
skill-frontmatter's files: pattern matched any SKILL.md, so it ran twice per sync -- once on the .apm/ source and again on the compiled flat-mirror copy. Scoped it to .apm/skills/ like its sibling hooks (skill-size-check, vale-audit-prefilter-skill), which already only check the source. sync-plugin-content.sh ran `apm pack` once per plugin, serially -- each invocation is dominated by fixed CLI startup cost rather than per-plugin work, so 6 plugins paid that cost 6 times over (~3.1s). Backgrounds the per-plugin work instead, buffering each plugin's output so concurrent DRIFT/FAIL messages can't interleave, then flushes in stable order after `wait` (~1.3s, confirmed idempotent on a real sync). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT |
||
|
|
b0936ad386 |
fix(kyberforge): force plugin.json regeneration on every real sync
apm pack silently skips regenerating plugin.json when it already exists
("already exists; skipping plugin.json generation. Re-run with --force to
overwrite it."). sync-plugin-content.sh never passed --force, so
.claude-plugin/plugin.json and .github/plugin/plugin.json were written
once on first sync and never refreshed again -- a name/version/description
edit in a plugin's apm.yml would silently stop propagating, with no gate
catching it (check-manifests.sh only checks presence; check-plugin-content-
sync explicitly excludes plugin.json; apm-pack-check-clean is scoped to
marketplace.json only).
Pass --force on real-mode syncs only (--check must not mutate the plugin
root, so plugin.json staleness there stays a known, undetected gap until
the next real sync -- documented in the script's header comment).
Regenerating surfaced a second, unrelated bug: apm's writer and
pretty-format-json's --autofix disagree on JSON key order (semantic vs.
alphabetical), so every real sync would otherwise flip plugin.json's key
order right back after a commit re-alphabetized it. Excluded the four
apm-generated manifests (plugin.json x2, marketplace.json x2) from
pretty-format-json -- their exact formatting is dictated by apm's own
writer as compiled output, not this repo's generic JSON prettifier.
Verified idempotent: a second real sync after this fix produces zero
further diff.
Refs: #90
|
||
|
|
5f42f57106 |
feat(kyberforge): enable codex marketplace output
Uncomment marketplace.outputs.codex in apm.yml. Codex output requires every package to declare a category -- added one per package (Developer Tools / Utilities / Version Control / Productivity, matching each plugin's actual scope). Re-running apm pack after enabling codex auto-synced 'codex' into each local package's own targets: list (apm.yml, per plugin) -- packages belonging to a marketplace that publishes a codex output need to declare codex as a supported compile target for that to be coherent. Verified idempotent: a second apm pack --check-clean run makes no further changes. .claude-plugin/marketplace.json regenerated (now carries category per plugin); .agents/plugins/marketplace.json is the new codex artifact. Refs: #90 |
||
|
|
6e77c11474 |
ci(kyberforge): add apm-native marketplace/audit/pack drift gates
Validated the plugin-content-mirror fix (issue #90) against apm's own packing/CI documentation and source: no apm-native mechanism replaces the mirror script (apm's bundler treats .apm/ and root convention dirs as mutually exclusive, by design), but the investigation surfaced a real, separate gap -- this repo ran zero apm-native audit/check commands in CI, relying entirely on custom scripts and Claude Code's own client-side validator. Add three pre-push hooks matching apm's documented producer CI pattern: - apm marketplace check: validates every marketplace.packages[] entry resolves, including live network reachability for remote refs -- a blind spot check-manifests.sh explicitly skips (local sources only). - apm audit --ci: apm's own lockfile/policy/hidden-content integrity gate. - apm pack --check-versions --check-clean: closes issue #90's deferred item 3 (a check-clean-equivalent gate) using apm's native flag instead of bespoke drift logic, verifying .claude-plugin/marketplace.json still matches what apm.yml + .apm/ would currently generate. All three are network-tolerant and whole-repo in scope, so they belong at pre-push alongside check-manifests/check-plugin-content-sync/ validate-plugins -- not pre-commit, which stays fast/offline/per-file. Documented the packing/bundling/releasing/CI findings in docs/research/docs/microsoft-apm/releasing.md (new) and extended testing-and-validation.md with the apm-action wrapper and its documented CI patterns, sourced from Context7 and cross-checked against the installed apm-cli 0.28.0 package directly. Refs: #90 |
||
|
|
38f1ba4e03 |
fix(kyberforge): bridge apm content to Claude Code's flat plugin discovery
Claude Code's (and Copilot's) native plugin installer has zero awareness of .apm/ nesting -- it convention-scans only flat skills/, agents/, commands/, hooks.json at each plugin's root. Confirmed via strings on the installed claude binary and live installs of git@holocron/gitea@holocron/kyberforge@ holocron, all reporting Skills(0) Agents(0) Hooks(0) post ADR-0015's apm conversion. Root cause (apm_cli/core/plugin_manifest.py): apm's plugin.json compiler deliberately strips skills/agents/commands keys, assuming the host already auto-discovers those convention directories -- it has no model of .apm/ being host-visible at all. Separately, apm's own bundle exporter (apm_cli/bundle/plugin_exporter.py, behind `apm pack --format plugin`) implements the correct .apm/ -> flat mapping, but only ever targeted build/<name>-<version>/, a path nothing in marketplace.json's source: points at. scripts/sync-plugin-content.sh wraps that bundle exporter and copies its agents/, skills/, commands/, instructions/, extensions/, and merged hooks.json back into each plugin's own root as a second tracked compiled-output category -- same governance status as .claude-plugin/plugin.json: generated from .apm/, never hand-edited. tests/ subdirectories are excluded from the mirror (dev fixtures, not host-visible runtime content; several hardcode a relative repo-root walk-up sized for the .apm/-nested depth, which breaks when duplicated one level shallower). Applied for real across all 6 plugins and verified two ways: `claude plugin validate --strict` passes on every real plugin directory, and a live `claude --plugin-dir <path> -p "list skills/agents"` behavioral test confirms content is now actually discovered. Also, from the same issue #90 review round: - scripts/check-manifests.sh pointed at each plugin's root-level plugin.json (checking skills/hooks/mcpServers/agents pointer fields) -- that file was a stale near-duplicate of .claude-plugin/plugin.json nothing else read or wrote, now deleted across all 6 plugins. check-manifests.sh is rewritten to validate .claude-plugin/plugin.json instead, and drops the pointer-field checks entirely (nothing to check -- those fields are correctly absent by design). Content-presence drift is now check-plugin-content-sync's job, a new pre-push hook wired in .pre-commit-config.yaml. docs/adr/0017 records the root cause and decision in full, including two rejected alternatives (patching plugin.json's path fields directly -- apm's compiler strips them on every run; pointing marketplace.json at apm pack's build/ output -- a version-suffixed non-source directory nothing can install from without an extra build step). ADR-0015 and CONTEXT.md are updated to point at it. Refs: #90 |
||
|
|
7910b8b12c |
docs(kyberforge): fix path drift and content gaps from apm conversion
The apm conversion (
|
||
|
|
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 <[email protected]> Claude-Session: https://claude.ai/code/session_01Ub96PyaSRD9BHPktotj1pC |
||
|
|
50d5c30a3c |
Merge pull request 'feat(kyberforge): retarget forge skills to author/audit APM content' (#93) from feat/89-apm-native-authoring into main
Reviewed-on: https://git.dev.rkdr.net/Defame1297/holocron/pulls/93 Reviewed-by: Defame1297 <[email protected]> |
||
|
|
eada85db99 |
docs(kyberforge): document source_keys as 4th apm-agent-allowlist field
field-inventory.md's apm-agent-allowlist and validate.sh's runtime check already included source_keys as a 4th allowed field, and the apm-agent.md template already instructed authors to add it for provenance tracking — but SKILL.md (x2), README.md, ADR-0016, and deployment-modes.md still described the allowlist as name/description/ model, "nothing else". The template itself even contradicted its own source_keys guidance with a header claiming "ONLY the three fields below — full stop" directly above it. Updates all six locations to document source_keys as the intentional 4th field, resolving the contradiction. |
||
|
|
044b2d3f08 |
fix(kyberforge): fix HOME/git scope-walkup false-FAILs in agent-audit
validate.sh's detect_scope() and validate-provenance.sh's find_plugin_root() disagreed with new-agent.sh's already-correct, documented walk-up semantics on three points, each causing validate.sh to false-FAIL a legitimately-scaffolded project-scope agent pair: - a marker-less directory walked up into $HOME (no .git/apm.yml of its own) was classified as user scope instead of project scope - the .git-boundary branch returned the walked-to .git location instead of the conventional scope root, breaking any <root> that is a subdirectory of a larger git-tracked tree (monorepo package dirs) - the new conventional-root arithmetic introduced to fix the above two cases had no guard against non-conventional/hand-placed file paths, which could point it at the wrong ancestor Also adds scripts/check-scope-walkup-sync.sh, a behavioral drift-guard (per ADR-0014's no-cross-skill-path precedent) that cross-checks the four independently hand-ported walk-up implementations (validate.sh, validate-provenance.sh, new-agent.sh, new-skill.sh) against real fixture scaffolds, wired into .pre-commit-config.yaml at pre-push so future drift between the ports is caught automatically. Verified via bash tests/run-tests.sh (13/13) and targeted before/after reproduction of each bug this closes. |
||
|
|
6f6b70781d |
fix(kyberforge): fix scope walk-up and manifest-parsing bugs from PR #93 review
A fresh /code-review of the APM-native authoring retarget (PR #93) found several correctness bugs beyond the ones already fixed on this branch: - new-agent.sh silently walked a marker-less subdirectory under $HOME up to user scope, contradicting its own usage text ("user scope is checked directly, no walk-up") and risking scaffolding into shared global ~/.claude or ~/.copilot directories instead of the intended local path. - The hand-copied apm.yml type: manifest detector in new-agent.sh and new-skill.sh accepted mismatched quotes (e.g. `type: "skill'`) that validate.sh's regex correctly rejects, and silently dropped a final apm.yml line lacking a trailing newline — causing the scaffolder and validator to disagree on scope for identical input. - Plugin-scope agent frontmatter could still contain the apm-agent.md template's HTML comments at ship time with no audit signal, yet apm compile copies frontmatter verbatim and <!-- --> breaks YAML parsing on both downstream harnesses. - ADR-0016 asserted agent-audit already implements a SUGGESTION heuristic for tool-restriction-needing plugin-scope agents; it doesn't. - agent-audit/README.md still described the old plugin-pair model this PR replaced with a single-file allowlist model. - validate.sh's project/user-scope CC-only/Copilot-only field checks and counterpart-missing check lost their only test coverage when the old plugin-pair fixture was deleted. Also replaces an echo-into-sed two-value parse (4 forks per call) with a single space-separated echo + read in both scaffolders. Regression tests added for every fix above, including one for a bug this pass introduced and the test suite caught: an initial two-line echo + `read` attempt silently dropped the second value, since `read` consumes only one line regardless of embedded newlines. Full suite: 158 bats tests, 39 shell-script tests, 12/12 summary categories, 0 failures. Refs: #89, #93 |
||
|
|
f037d49b5c |
fix(kyberforge): fix apm-scope validation gaps in agent/skill authoring scripts
Post-implementation review of PR #93 (issue #89's apm.yml-native retargeting of skill-author/skill-audit/agent-author/agent-audit) found four confirmed defects across the four scripts' apm.yml `type:` walk-up logic: - field-inventory.md's apm-agent-allowlist was missing `source_keys`, contradicting agent-author/SKILL.md's own instruction (Step 5 checklist) to allow it at plugin/APM scope — a correctly-authored file with source_keys failed validate.sh. - validate.sh's APM_TYPE_RE and validate-provenance.sh's TYPE_RE disagreed: the former tolerated a quoted `type: "skill"` value, the latter didn't, despite agent-audit/SKILL.md explicitly documenting that validate-provenance.sh walks up "the same way validate.sh does". Both also used `\b` word-boundary matching, which false-matches a malformed value like `type: prompts-only` on the `prompts` prefix. Unified both regexes to be quote-tolerant and require an exact value. - All four scripts' `.git` project-boundary check used isdir()/[[ -d ]], which misses git worktrees where `.git` is a regular file (`gitdir: ...`) rather than a directory. Switched to exists()/[[ -e ]]. - new-agent.sh and new-skill.sh had the same quote-intolerance as above via inline `grep -qE` calls (new-skill.sh's also had the `\b` false-match bug); replaced both with a shared-shape `is_apm_package_manifest` bash helper matching the Python regex's semantics. Four other findings from the same review turned out not to be bugs: a bare `plugin.json` no longer signaling plugin scope is documented, intentional behavior (agent-audit/SKILL.md:30, agent-author/SKILL.md:87), deferred to issue #90's real plugin.json-to-apm.yml conversion — not something this fix should reverse. Verified via direct reproduction of each defect plus the full test suite: 147/147 bats tests, 39/39 shell-script tests, 12/12 summary categories. Refs: #89 |
||
|
|
96bc946030 |
docs(kyberforge): fix provenance chain gap in agent-primitive-schema.md
The file's body cites APM's Python source directly (apm_cli/models/validation.py, apm_cli/primitives/models.py) to support its "no dedicated Agent validation" claim, but source_keys only listed context7-microsoft-apm — the apm-github-repo source_key was missing, and sources.md's apm-github-repo entry didn't list this file under Contributing files either. The three sibling schema files from the same commit (prompt/instructions/hooks) all cited both sources correctly. Found via post-implementation review of issue #89. |
||
|
|
ffebdc6584 |
fix(kyberforge): stop dotfiles-repo $HOME from shadowing user scope in validate.sh
detect_scope() had the same bug class fixed in new-agent.sh ( |
||
|
|
dc2a41034e |
fix(kyberforge): remove dead plugin-scope branch from validate.sh
check_file()'s is_plugin_scope param and its plugin-silently-ignored field check were unreachable dead code left over from the issue #89 restructure: plugin/APM scope now exits via check_apm_agent_file() before check_file() is ever called, so is_plugin was always False. Remove the param, its branch, the unused plugin_ignored_fields parse, the now-stale field-inventory.md section, and the SKILL.md mention. Found via post-implementation review of issue #89. |