d654dca0564cb589be71795091bb9ee5602cc1cc
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| ea119d83b0 |
fix(gates): close six PR #135 review findings in gates and their tests
B1: check-skill-version-bump.sh resolves every merge-base with `git merge-base
--all` instead of the single base git happens to pick. A criss-cross history has
two, so the verdict turned on that choice: a skill byte-identical to main's tip
could still be reported "not above merge-base" / "not above main tip" and fail a
push that should pass. A skill now counts as changed only when it differs from
EVERY base, and its version must exceed the version at every base it exists at
as well as at the main tip; with more than one base the failure names which one.
Case 40 in tests/test-skill-version-bump.sh builds the criss-cross fixture and
pins both directions.
B2: check-apm-current.sh no longer assumes the remote default branch is `main`
when origin/HEAD is unset. A checkout whose default is `master` was standing on
its default branch and being told "this is a feature branch, so discard it" --
to throw away a real lock update. With origin/HEAD unset nothing is asserted and
the neutral advice stands. tests/test-apm-current-hook.sh covers the unset case
on both `main` and `master`.
#4: the required-frontmatter checks folded into skill-size-check.sh by
|
|||
| 807caf22ee |
fix(kyberforge): give branch-aware advice for the refreshed apm lock
Why: the docs said to discard a refreshed apm.lock.yaml on a feature branch because the refresh records main's commit, but the branch's own lock records a (older) main commit too, and the SessionStart notice gave the same "commit or discard" advice on every branch. Implementation Notes: - check-apm-current.sh picks fixed advice by branch: commit or discard deliberately on the default branch (origin/HEAD, else main), discard and reinstall on a feature branch; the branch name is never interpolated. - README, AGENTS.md and ADR-0019 give the real reasons (no lock churn in the branch diff, deployed tree matches the committed lock), the cost (the session runs the older main) and that the next session start refreshes again. - ADR-0019's check-clean and stale-server claims restated to match apm's source. ADR: 0019 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 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 |
|||
| 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 |
|||
| 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 |