Defame1297 ffcbed6c41 fix(tests): replace pipefail-racy echo | grep -q with here-strings
Why

Two suites failed intermittently — tests/test-vale-wrap.sh case 21 and
tests/test-check-release-needed.sh cases 4 and 15 — on correct output, and never
when run alone. The cause is the `echo "$OUT" | grep -q P` idiom under
`set -o pipefail`: grep -q exits as soon as it has an answer, bash's echo can
hand a multi-line value to the pipe one line at a time, and a write after the
reader is gone kills echo with SIGPIPE. pipefail then reports the writer's
death, so output that DID match reads as "no match". Every observed failure had
lines after its match; case 15's match is on line 1 of 6, the widest window in
that file.

Forced with a pause before the writer's last line, the pipe form failed 50 of 50
runs; a here-string, a match on the last line, and the same pipe without
pipefail each passed 50 of 50. Unforced the rate is about 1 per 670 suite runs,
which is why it read as a flaky gate rather than a bug.

The failures at review time are consistent with this, but were not proven to be
it: the suite was running while agents edited live config files in place, and a
brief change to .vale.ini or .pre-commit-hooks.yaml would produce the same two
failures. The race is real and fixed either way.

Implementation Notes

`grep -q P <<< "$VAR"` has no separate writer process, so there is nothing to
race. It is not a retry or a sleep. 121 sites converted across 9 files, three of
them scripts rather than tests: new-agent.sh, new-skill.sh and
check-executables-allow-sync.sh. None ships via .pre-commit-hooks.yaml, so no
external consumer pins them, and all three are single-pipeline checks whose
verdict cannot change.

Left alone deliberately: 14 sites whose writer is a command, not a shell
builtin — they either absorb the writer's status with `|| true` or are python3
and awk, which write once at exit — and one file with no pipefail. `printf '%s'`
sites differ from a here-string only by a trailing newline, which no -q verdict
on a non-empty pattern depends on.

tests/test-no-pipefail-early-exit-grep.sh is a static guard against new
occurrences, discovered automatically by run-tests.sh. It only scans files that
set pipefail, joins continuation lines, skips comments, and flags only
echo/printf writers. Its first case proves the scanner can fail before its
second trusts a clean verdict on the tree.

A guard covers exactly the spellings its regex models, so the miss surface was
measured rather than assumed. Four were found and closed: pipefail declared as
`set -o errexit -o pipefail` (where the old pattern required pipefail to follow
the FIRST -o, and a file-level miss skips every site in that file); a writer
separated from grep by an intermediate stage; a pipeline wrapped on a trailing
`|` rather than a backslash; and readers spelled egrep, fgrep, /bin/grep,
`command grep` or with an env-var prefix. Segment characters exclude a bare `&`
so `echo ok && other | grep -q x`, whose writer is `other`, does not false-fire.
Widening surfaced 5 live sites invisible to the original scanner, all in
tests/test-apm-current-hook.sh, all `echo "$out" | json_field ... | grep -q`;
they are safe today only because json_field is python3, which reads to EOF and
writes once. Fixtures go 4 to 12 vulnerable spellings plus near-miss negatives.

Two `grep ... | head -1` sites (test-vale-wrap.sh) are the same race with a
different early-exiting reader, and are fixed by absorbing the writer. The
scanner deliberately does not model `head`, `sed -n 1p` or a bare `read`: most
legitimate uses in this tree are already absorbed with `|| true` and the scanner
cannot see absorption from pipeline text, so a high false-positive rate would be
how this guard gets weakened. Heredoc bodies are scanned as code; none in the
tree trips it today.

Impact

The bug predates the factory-audit merge: every converted site in
check-release-needed and case 21 dates to 4d018af and aa8cc22 (2026-08-09).

Test suites go 19 to 20. `run-tests.sh --strict` passes 20/20 with 0 skipped,
four consecutive runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
2026-09-16 09:14:01 +00:00

holocron

The global AI development configuration repository — the authoritative source for agent definitions, skills, workflows, and prompts across all projects. Built as a homelab tool intended to scale to professional environments.

Content ships as six installable plugins, each an apm (Agent Package Manager) package. This repo consumes its own plugins through apm, so the working copy runs the same released content every other consumer gets.

Repo layout

Path What it holds
plugins/ Six apm packages — bin, core, git, gitea, kyberforge, lint — each carrying skills, and where relevant agents, hooks, and bundled assets
providers/claude-code/ Claude Code adapter, deployed to ~/.claude/ via scripts/install.sh
core/ Provider-agnostic always-on content — core/AGENTS.md and core/instructions/
docs/ Specs (docs/spec/), architectural decisions (docs/adr/), governance, research, and notes
scripts/ Install, sync, and check scripts used by the git hooks
tests/ run-tests.sh, run-bats.sh, the test-*.sh suites, and the bats submodules

The six plugins:

  • kyberforge — skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace
  • git — conventional commits, branches, history, submodules, worktrees, remotes, pre-commit hook authoring and running (pc-author / pc-run), and an interactive router (git-workflow)
  • gitea — issues, pull requests, labels, milestones, releases, branches, files, and an interactive router (gitea-workflow)
  • core — authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it
  • lint — configuring and running linters
  • bin — cross-cutting workflow skills not yet split into a focused plugin: research, documentation, TDD, prototyping, triage, diagnosis, architecture review, requirement grilling, compressed output (caveman), and re-orienting mid-task (zoom-out)

Prerequisites

Install all of these before setting up. Each one is a hard dependency of a git hook or a script — several fail with an unhelpful "command not found" if missing.

Tool Why Install
apm CLI Two pre-push hooks shell out to it (apm-audit-ci and apm-pack-check-clean) The apm-install skill, or curl -sSL https://aka.ms/apm-unix | sh. Verify with apm --version
python3 + PyYAML Required by scripts/skill-size-check.sh (the skill-size-check pre-commit hook), which reads folded YAML frontmatter python3 is usually present — pre-commit is itself a Python application. pip install pyyaml if the hook reports PyYAML missing
vale Required by the vale-audit-prefilter-skill / -agent pre-commit hooks, and by the test-vale-wrap.sh / test-vale-hooks-consumer.sh suites that run-tests --strict runs at pre-push brew install vale (macOS), snap install vale (Linux), choco install vale (Windows), or https://vale.sh/docs/vale-cli/installation/
claude CLI Required by the validate-marketplace pre-push hook Claude Code

Two notes worth reading before you skip one:

  • PyYAML is a hard requirement, not an optional accelerator. The hand-rolled fallback frontmatter reader was removed deliberately: a reader that mis-parses an unfamiliar scalar shape reports a clean pass on a file it never measured.
  • No vale sync is needed. The Kyberforge styles are committed under plugins/kyberforge/.apm/skills/factory-audit/assets/vale/styles/, not downloaded packages (ADR-0014, ADR-0025).

Setup

Run these in order, from the repo root.

# 1. Deploy this repo's own skills and agents
apm install

# 2. Install the git hooks — all three stages
pre-commit install -t pre-commit -t commit-msg -t pre-push

apm install deploys the six plugins into .claude/skills/ and .claude/agents/. Both are gitignored install output, not authoring source — plugins/<name>/.apm/ remains the only place to edit. It needs the network and materializes apm_modules/ (which stays gitignored).

Git hooks must be wired for all three stages. This repo's .pre-commit-config.yaml has no default_install_hook_types, so a plain pre-commit install silently skips commit-msg (Conventional Commits) and pre-push (the full gate) — the -t flags above are not optional. The pc-run skill handles this and the troubleshooting around it, if you would rather not remember the flags.

Keeping the install current

The six dependencies in root apm.yml are unpinned against the default branch, so deployed skills go stale whenever anyone merges. kyberforge's SessionStart hook keeps the install current automatically on launch, rewriting apm.lock.yaml in the process — an unexplained modification to it after opening a session is expected, not a bug; commit or discard it deliberately. Mechanism and rationale: docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md.

Note the difference between the two commands:

  • apm install deploys from apm.lock.yaml. It does not pick up remote changes.
  • apm update re-resolves refs. This is the command that pulls in a merged .apm/ edit.

Running tests

bash tests/run-tests.sh              # every test-*.sh script plus the bats suite
bash tests/run-tests.sh --bats-only  # just bats

The first run auto-initializes the bats submodules; no manual git submodule update needed.

A suite that exits 77 because a dependency is missing is reported as SKIPPED and does not fail an ad-hoc run. It does fail under --strict (equivalently RUN_TESTS_STRICT=1), which is how the pre-push hook invokes it — at pre-push, a skip means one of the prerequisites above is absent on this machine, and the gate would otherwise report success having run fewer suites than it appears to. The strict failure names each skipped suite and what to install.

Before pushing

Run the pre-push gate locally in one command:

pre-commit run --hook-stage pre-push --all-files

One caveat: check-release-needed is a silent no-op under this invocation. It exits 0 unless PRE_COMMIT_REMOTE_BRANCH is refs/heads/main, and pre-commit exports that only from the real pre-push git hook during an actual git push — so the hook reports Passed having checked nothing. Every other pre-push hook does run.

See docs/spec/gates.md for what each hook enforces and why.

Offline? No pre-push hook needs the network: root apm.yml's marketplace has no remote package entries (the last one, mattpocock-skills, was removed), so apm-pack-check-clean resolves everything from local sources. All pre-push hooks pass offline.

Editing plugin content

plugins/<name>/.apm/ is the only hand-edited source for plugin content — the root marketplace.json manifest is generated by apm pack, and a hand-edit there is reported as drift by apm-pack-check-clean. Hand-authored material that is not an .apm/ primitive (README.md, docs/, bin/, sources.md) lives at the plugin root instead.

Full model, including what's exempt and why: docs/spec/architecture.md.

For external consumers

Consume the packages through apm, the way this repo does — declare them as dependencies.apm git+path entries against the holocron remote and run apm install. apm is the only supported install path.

Where to go next

Description
AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.
Readme 14 MiB
Languages
Shell 89.7%
Python 6.3%
HTML 3.7%
JavaScript 0.3%