Residual low-severity items from PR #95's fourth review round #97
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Six residuals from PR #95's final integration review. All Low or Informational; none gated that PR. Grouped here so they are not lost.
1. One new guard has no test (the round's only piece of decoration)
scripts/check-vale-style-sync.sh:74-79— the unreadable-.vale.iniguard. Replacingif [[ ! -r "$ini" ]]withif falseleaves the suite green.Not fixed in PR #95 deliberately: the suite runs as root in the dev environment, and root's
[[ -r ]]returns true on achmod 000file, so a mode-based test would pass or fail depending on invoking uid. A flaky test is worse than an untested three-line guard.Needs either a non-uid-dependent test (e.g. a directory where a file is expected, or an unreadable path via a bind mount in CI) or an explicit in-file note that it is untested by design.
2. Stale
# shellcheck source=directive silently disarms an exemption pathtests/run-tests.sh:61declaressource=lib/batch-run.sh, which resolves to neither$REPO_ROOT/lib/batch-run.shnortests/lib/batch-run.sh— both absent. The correct spelling is attests/run-bats.sh:54(../scripts/lib/batch-run.sh).Consequence: the
sourced_files()exemption intest-vale-wrap.sh's bash-3.2 scan cannot seebatch-run.sh's array seeding when scanningrun-tests.sh. No live miss today — both ofrun-tests.sh's expansions are already guarded andbatch-run.shis scanned directly — but the exemption path is untested against a real resolution failure, andshellcheck's SC1091 is below the repo's--severity=warningthreshold so nothing flags it.3. Stray content inside the new
hooks/directory is uncheckedsync-plugin-content.sh'schecked_pathscoversMIRROR_DIRS+hooks/hooks.json+ the legacyhooks.json, but not thehooks/directory itself. Measured:plugins/kyberforge/hooks/extra.json→--checkexit 0, real sync leaves itplugins/core/hooks/→--checkexit 0, real sync leaves itCheck and sync agree in both cases, so the invariant holds and this is not a correctness break — but a stray file in a directory the mirror now owns is invisible to the gate. Contrast with a stray at the plugin root, which is out of scope by design (
README.md,docs/,bin/,sources.md,.mcp.jsonare hand-authored).4.
promptsdocumented as mirrored but absent fromMIRROR_DIRSscripts/sync-plugin-content.sh:4says it mirrors.apm/{agents,skills,prompts,commands,instructions,extensions,hooks};:115isMIRROR_DIRS=(agents skills commands instructions extensions).A plugin adding
.apm/prompts/would silently never be mirrored, and--checkwould report clean. No plugin has one today. Either add it or correct the header.5. Test suite is ~2× slower
Wall time 114s. The cost is real coverage (16 Vale override fixtures, each running vale), and for an
always_runpre-push gate on a generator whose output ships to users that is a defensible trade — but worth knowing where it went:test-check-vale-style-sync.shtest-sync-plugin-content.shtest-check-scope-walkup-sync.shtest-vale-wrap.shtest-check-vale-style-sync.shis now the single wall-clock bottleneck.6.
AGENTS.mdsays 12 pre-push hooks; the command it recommends reports 14The repo defines 12, but
check-hooks-applyandcheck-useless-excludes(meta repo, nostages:restriction) also run at pre-push. A reader following the instruction sees a mismatch on the first try. Either say "12 repo-defined (14 including pre-commit's meta hooks)" or drop the count.Suggested order: 6 and 2 are one-line fixes; 4 is a decision (add vs. correct); 1 and 3 need a little thought; 5 is informational unless the pre-push time becomes annoying.
All six items addressed on
feat/90-execute-apm-conversion, folded into PR #95 and disclosed there. Three of the six did not survive verification as written — details below, because two of them would send the next reader down a wrong path.-rguardc442f7esource=directive73393b9hooks/5a61b41promptsmissing fromMIRROR_DIRS5a61b41c442f7e0f0ac58Item 4 — refuted, no
MIRROR_DIRSchange madeIt would be mirrored.
MIRROR_DIRSlists destination directory names at the plugin root; the header comment lists source directories under.apm/. The two lists are legitimately different, andpromptsis the one entry where they diverge: apm folds.apm/prompts/intocommands/alongside.apm/commands/, renaming*.prompt.md→*.md. ADR-0017:49 already documents this mapping.Verified empirically rather than read off the ADR — a fixture carrying
.apm/prompts/greet.prompt.mdproducescommands/greet.mdand noprompts/directory at the plugin root.Adding the
promptsentry this item asks for would have named an output directory apm never emits and no plugin host scans. Instead: a comment atMIRROR_DIRSexplaining why the lists differ, and a characterization test pinning the mapping — so an apm upgrade that gavepromptsa destination of its own fails loudly, which is the only condition under which the entry would actually be needed.Item 2 — the recommended spelling is also broken
It is not. Directives resolve against the source-path, which under pre-commit is the repo root, so
../scripts/...escapes the repo:Both spellings were broken.
run-bats.shonly looked correct becausetest-vale-wrap.sh'ssourced_files()tries the script's own directory as a second candidate — a heuristic shellcheck itself does not share. The spelling that satisfies both is repo-root-relative (scripts/lib/batch-run.sh), matchingscripts/install.sh:5.A third instance the issue does not mention,
scripts/check-manifests.sh:49, was broken the same way. All now resolve, asserted by a new case that holds every directive in the scanned corpus to the resolution rule — so the next stale one fails at test time rather than lying dormant. Confirmed it kills the original mutation: restoring the old spelling givesFAIL … tests/run-tests.sh (0/1).Item 1 — the guard was dead code
The issue frames this as untestable-because-root. The sharper statement is that it was unreachable because of root:
[[ -r ]]isaccess(2)— it reports whether the permission bits would allow a read. For uid 0 that is yes even on a mode-000 file (verified:[[ -r ]]true andcatsucceeds). This hook runs at pre-push and the environment is root, so the guard could never fire in the one place it exists to fire. There was no uid-independent test for it because there was nothing to test.Readability is now decided by actually reading (
cat), which is uid-independent and strictly stronger — it catchesEISDIRandEIO, whichaccess(2)reports on neither.cat, not a< "$ini"redirect: opening a directory for reading succeeds, only the read fails. The missing branch moved to-e, so a directory sitting where the file belongs is reported as unreadable rather than as deleted.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 report a missing
StylesPathfor a file that has one. An exit-code-only test would have been green with the guard deleted, i.e. exactly the failure mode this repo keeps finding.Ruled out by experiment, not assumption: directory / broken symlink / symlink loop all fail
-fand get caught by the preceding branch;/proc/self/memis Linux-only and madegrephang; privilege-drop is Linux-only and would drag the whole fixture tree's permissions into scope.Item 5 — fixed locally, but the suite got slower overall
test-check-vale-style-sync.sh: 51s → 32s, by masking vale in the 21 of 28 script runs that only assert.vale.initext (thePATH_NO_VALEmechanism case 12 already builds). The helper falls back to an unmasked run rather than skipping, so a machine where masking is unavailable loses speed, never coverage.But the suite went 114s → 129s. The saving was more than eaten by the tests added for items 1 and 3:
test-sync-plugin-content.shwent 37s → 64s, since each newhooks/-stray andpromptscase runs a realapm pack. The bottleneck moved fromtest-check-vale-style-sync.shtotest-sync-plugin-content.shrather than disappearing. Recording this plainly because "3.5× faster" is true of the file and false of the suite.The masking turned out to matter more for coverage than for time. With vale on
PATH, cases 8 and 9 could not detect deletion of the assertions they were written to catch: a droppedStylesPathalso 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.Remaining unclaimed win: cases 11 and 11b need no vale either (~2 more runs, ~7%).
Item 3 — fixed, and it immediately found real strays
hooks/is now wiped and rebuilt like everyMIRROR_DIRSdestination, and the directory itself (not justhooks/hooks.json) is inchecked_paths, so the recursive manifest sees one-sided entries. Both measured cases in the issue now report drift and are cleaned by a real sync.On first run the new gate flagged three empty
plugins/{git,gitea,core}/hooks/directories. Untracked local cruft rather than anything committed — git does not track empty directories, so a fresh clone never had them — but the gate working on its first outing.One defect introduced while fixing this and caught before commit: the new
rm -rf "$target_dir/$HOOKS_DIR_REL"trippedSC2115.set -uaborts on an unset variable but not an empty one, so an empty$target_dirwould make itrm -rf /hooks. Guarded with${target_dir:?}.Item 6
AGENTS.md now says 12 repo-defined hooks and explains that
pre-commit's ownmetahooks (check-hooks-apply,check-useless-excludes) declare nostages:and so also run at pre-push, which is why the recommended command reports 14.Verification: full suite 16/16, 0 failures; all 14 pre-push hooks pass including
apm marketplace check,apm audit --ci,apm pack --check-cleanand bothclaude-CLI validators;shellcheck --severity=warningclean on all nine changed shell files. Every new guard was mutation-tested to fail under the defect it exists to catch, not merely to pass.