Release v1.34.0: grill nudge no longer displaces situation-matched sk… #139
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: verify | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| jobs: | |
| verify: | |
| runs-on: ubuntu-latest | |
| steps: | |
| # fetch-depth 0 because check 24 compares the installable payload against the tag whose | |
| # version the manifests declare, and the default shallow checkout carries no tags at all. | |
| # Without this the check cannot measure and skips, which is honest but proves nothing on | |
| # the one machine that gates every push. | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| # The gate needs jq for the JSON checks and the install dry run. shellcheck backs check 29, | |
| # which skips without it -- and a check that skips on the one machine gating every push is | |
| # a green measuring nothing, which is the failure mode this repository keeps rediscovering. | |
| # `shellcheck --version` runs so a broken install stops the run here rather than being | |
| # found later as a check that quietly proved nothing. | |
| - name: Install jq and shellcheck | |
| run: | | |
| sudo apt-get update -qq && sudo apt-get install -y -qq jq shellcheck | |
| shellcheck --version | |
| # Check 19 runs `claude plugin validate --strict`; without a working CLI it skips, and the | |
| # falsifiability suite skips its case 19 with it. Install it so CI proves the real thing. | |
| # | |
| # `npm install -g` alone was not enough and produced a false green for as long as the step | |
| # has existed. The npm package is a shim that fetches a platform-native binary in its | |
| # postinstall; that did not land here, so every `claude plugin validate` exited non-zero | |
| # with "claude native binary not installed" — and check 19's old output parsing read that | |
| # error as no findings and printed ok. CI has never once validated these manifests. | |
| # | |
| # So install it, then prove the binary answers before trusting the step. `claude | |
| # --version` failing here is a setup failure and should stop the run, not be discovered | |
| # later as a check that silently measures nothing. | |
| - name: Install Claude Code CLI | |
| run: | | |
| set -u | |
| # The npm package is a shim; the real binary arrives as a platform-specific optional | |
| # dependency. Installing the shim alone produced a false green for as long as this | |
| # step existed: the optional dep never landed, every `claude plugin validate` exited | |
| # with "native binary not installed", and check 19's old output parsing read that | |
| # error as "no findings" and printed ok. CI validated these manifests exactly zero | |
| # times before this was found. | |
| # | |
| # The reason the optional dep never landed is worth naming, because it will recur: | |
| # the shim pins its platform deps to its own version, and the platform packages are | |
| # published on a lag. At the time of writing the shim was 2.1.237 and linux-x64 was | |
| # 2.1.236, so npm resolved nothing and said nothing. Ask for the platform package by | |
| # name at whatever version is actually published, and use the binary it ships — it is | |
| # self-contained and needs no shim. | |
| npm install -g @anthropic-ai/claude-code || true | |
| case "$(uname -s)-$(uname -m)" in | |
| Linux-x86_64) PKG=linux-x64 ;; | |
| Linux-aarch64) PKG=linux-arm64 ;; | |
| Darwin-arm64) PKG=darwin-arm64 ;; | |
| Darwin-x86_64) PKG=darwin-x64 ;; | |
| *) PKG="" ;; | |
| esac | |
| if [ -n "$PKG" ]; then | |
| PREFIX="$RUNNER_TEMP/ccbin" | |
| npm install --prefix "$PREFIX" "@anthropic-ai/claude-code-$PKG" || true | |
| BIN="$PREFIX/node_modules/@anthropic-ai/claude-code-$PKG/claude" | |
| if [ -x "$BIN" ] && "$BIN" --version >/dev/null 2>&1; then | |
| sudo ln -sf "$BIN" /usr/local/bin/claude | |
| fi | |
| fi | |
| # Report rather than fail. Check 19 runs its own two-way control and skips with a | |
| # reason when the validator is unusable, so an unavailable CLI degrades to a visible | |
| # skip in the gate output instead of a red run over something this repo does not own. | |
| claude --version || echo "::warning::claude CLI unusable on this runner; check 19 will skip" | |
| - name: The manifests were actually validated, not skipped | |
| run: | | |
| # Check 19 is allowed to skip, so a green gate does not prove it ran. On this runner | |
| # it must run: that is the whole point of installing the CLI above, and a silent | |
| # regression back to skipping is exactly how it went unnoticed before. | |
| ./.claude/verify.sh > "$RUNNER_TEMP/gate.txt" 2>&1 || true | |
| if grep -q '^skip plugin manifests valid' "$RUNNER_TEMP/gate.txt"; then | |
| grep '^skip plugin manifests valid' "$RUNNER_TEMP/gate.txt" | |
| echo "check 19 skipped on a runner where the CLI was installed on purpose" | |
| exit 1 | |
| fi | |
| grep '^ok plugin manifests valid' "$RUNNER_TEMP/gate.txt" | |
| - name: Run the verification gate | |
| run: ./.claude/verify.sh | |
| # A gate that cannot fail proves nothing. This used to break one skill description and | |
| # call it done, which left the other checks unproven — and two of them were not enforcing | |
| # anything at the time. Every declared check now gets its own mutation, and check 16 of | |
| # the gate fails if a check is added without one. | |
| - name: Prove every check is falsifiable | |
| run: ./tests/gate-falsifiability.sh | |
| # The comparison the README cites. It asserts the decision each mechanism produces, so a | |
| # regression turns it red rather than quietly changing a number in a table nobody re-runs. | |
| - name: The mechanisms still decide what the README says they do | |
| run: ./tests/compare-baseline.sh | |
| # A standalone reproduction of the delegation mandate in skill-mandate.sh, offline and | |
| # under 2s. It is not a pass/fail harness on its own -- check 27 of the gate above is the | |
| # thing that actually enforces the four cases it drives -- but it must at least run clean | |
| # so its raw stdout/exit code stays trustworthy for anyone reaching for it by hand. | |
| - name: skill-mandate.sh breadth reproduction runs clean | |
| run: ./tests/test-breadth-mandate.sh | |
| # Zero model calls, fixture integrity only. auto-trigger.sh and team-gating.sh need a | |
| # billed, authenticated `claude` session the user has declined to give CI, so this is the | |
| # one thing about skill dispatch this pipeline CAN check for free: that the fixtures those | |
| # suites depend on (skill names, setup_* functions, the files they write, the case count | |
| # their own docs claim) are still intact. See the header of dispatch-static.sh for what | |
| # this does and does not prove -- it does not prove a skill fires. | |
| - name: auto-trigger.sh fixtures are still intact (no model calls) | |
| run: ./tests/dispatch-static.sh | |
| - name: Confirm the tree is restored | |
| run: git diff --exit-code | |
| # Everything above runs the gate. This installs for real, on Linux, into a disposable HOME. | |
| # It exists because the worst bug this repo has shipped was Linux-only and invisible on the | |
| # machine it was written on: hooks called /usr/bin/jq by absolute path, which is a macOS | |
| # path, so off macOS the verify gate silently stopped blocking and the session hook injected | |
| # nothing at all. A dry run cannot catch that. Only installing and firing the hooks can. | |
| install-linux: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install jq | |
| run: sudo apt-get update -qq && sudo apt-get install -y -qq jq | |
| # Runs install.sh into throwaway HOMEs and asserts the resulting tree each time: a plain | |
| # install, CLAUDE_CONFIG_DIR pointed elsewhere, a home path with a space, jq removed from | |
| # PATH, a bash user, a second run for idempotency, both uninstall paths, and an install | |
| # over a home that already holds somebody else's skills, settings and MCP servers. Two | |
| # further cases exercise the curl bootstrap and the plugin marketplace against the | |
| # published repo. This runner is the environment the matrix exists for — bash by default, | |
| # no Homebrew, the shape of a cloud VM or a container. | |
| - name: Install Claude Code CLI | |
| run: | | |
| set -u | |
| # The npm package is a shim; the real binary arrives as a platform-specific optional | |
| # dependency. Installing the shim alone produced a false green for as long as this | |
| # step existed: the optional dep never landed, every `claude plugin validate` exited | |
| # with "native binary not installed", and check 19's old output parsing read that | |
| # error as "no findings" and printed ok. CI validated these manifests exactly zero | |
| # times before this was found. | |
| # | |
| # The reason the optional dep never landed is worth naming, because it will recur: | |
| # the shim pins its platform deps to its own version, and the platform packages are | |
| # published on a lag. At the time of writing the shim was 2.1.237 and linux-x64 was | |
| # 2.1.236, so npm resolved nothing and said nothing. Ask for the platform package by | |
| # name at whatever version is actually published, and use the binary it ships — it is | |
| # self-contained and needs no shim. | |
| npm install -g @anthropic-ai/claude-code || true | |
| case "$(uname -s)-$(uname -m)" in | |
| Linux-x86_64) PKG=linux-x64 ;; | |
| Linux-aarch64) PKG=linux-arm64 ;; | |
| Darwin-arm64) PKG=darwin-arm64 ;; | |
| Darwin-x86_64) PKG=darwin-x64 ;; | |
| *) PKG="" ;; | |
| esac | |
| if [ -n "$PKG" ]; then | |
| PREFIX="$RUNNER_TEMP/ccbin" | |
| npm install --prefix "$PREFIX" "@anthropic-ai/claude-code-$PKG" || true | |
| BIN="$PREFIX/node_modules/@anthropic-ai/claude-code-$PKG/claude" | |
| if [ -x "$BIN" ] && "$BIN" --version >/dev/null 2>&1; then | |
| sudo ln -sf "$BIN" /usr/local/bin/claude | |
| fi | |
| fi | |
| # Report rather than fail. Check 19 runs its own two-way control and skips with a | |
| # reason when the validator is unusable, so an unavailable CLI degrades to a visible | |
| # skip in the gate output instead of a red run over something this repo does not own. | |
| claude --version || echo "::warning::claude CLI unusable on this runner; check 19 will skip" | |
| - name: Install matrix (all environments, plus the network lanes) | |
| run: ./tests/install-matrix.sh | |
| # The marketplace case is allowed to skip when there is no CLI, which meant it skipped in | |
| # every job: the CLI was installed only in the verify job, and the matrix ran only in the | |
| # others. One of the three advertised install lanes was therefore unproven while every | |
| # required check stayed green. The CLI is installed above on purpose, so a skip here is a | |
| # regression, not an environment fact. | |
| - name: The marketplace lane actually ran | |
| run: | | |
| ./tests/install-matrix.sh marketplace > "$RUNNER_TEMP/mkt.txt" 2>&1 || true | |
| cat "$RUNNER_TEMP/mkt.txt" | |
| if grep -q '^skip plugin marketplace lane' "$RUNNER_TEMP/mkt.txt"; then | |
| echo "marketplace case skipped on a runner where the CLI was installed on purpose" | |
| exit 1 | |
| fi | |
| grep -q '^ok plugin marketplace lane' "$RUNNER_TEMP/mkt.txt" | |
| - name: install.sh, for real | |
| run: ./install.sh | |
| - name: The installed tree matches the repo | |
| run: ./bin/doctor --drift | |
| # Expected counts are derived from the repo at run time, not hardcoded — a literal here | |
| # went stale the first time a skill was added and failed a green tree. | |
| - name: Everything landed | |
| run: | | |
| set -e | |
| count() { ls -d $1 2>/dev/null | wc -l | tr -d ' '; } | |
| for spec in "$HOME/.claude/skills/*/:claude/skills/*/" \ | |
| "$HOME/.claude/agents/*.md:claude/agents/*.md" \ | |
| "$HOME/.claude/commands/*.md:claude/commands/*.md" \ | |
| "$HOME/.claude/hooks/*.sh:claude/hooks/*.sh"; do | |
| pat="${spec%:*}"; src="${spec##*:}" | |
| got=$(count "$pat"); want=$(count "$src") | |
| [ "$got" = "$want" ] && [ "$want" != 0 ] || { echo "$pat: got $got, repo has $want"; exit 1; } | |
| echo "ok $pat -> $got (repo: $want)" | |
| done | |
| test -x "$HOME/.claude/hooks/verify-gate.sh" | |
| - name: The Stop gate blocks on Linux, with and without jq on PATH | |
| run: | | |
| set -e | |
| mkdir -p /tmp/g/repo/.claude /tmp/g/home/.config/agents /tmp/g/tmp /tmp/g/bin | |
| printf '#!/usr/bin/env bash\necho "seeded failure"\nexit 1\n' > /tmp/g/repo/.claude/verify.sh | |
| chmod +x /tmp/g/repo/.claude/verify.sh | |
| printf '%s %s\n' "$(sha256sum /tmp/g/repo/.claude/verify.sh | cut -d' ' -f1)" \ | |
| /tmp/g/repo/.claude/verify.sh > /tmp/g/home/.config/agents/verify-trust | |
| for t in bash sh cat cut grep sed awk tr rm mkdir env dirname basename sha256sum; do | |
| ln -sf "$(command -v $t)" /tmp/g/bin/$t | |
| done | |
| out=$(printf '{"session_id":"ci"}' | env HOME=/tmp/g/home TMPDIR=/tmp/g/tmp \ | |
| CLAUDE_PROJECT_DIR=/tmp/g/repo bash claude/hooks/verify-gate.sh) | |
| echo "$out" | jq -e '.decision=="block"' >/dev/null || { echo "did not block with jq"; exit 1; } | |
| echo "ok blocks with jq" | |
| sed 's#/usr/bin/jq#/nonexistent/jq#' claude/hooks/verify-gate.sh > /tmp/g/nojq.sh | |
| out=$(printf '{"session_id":"ci2"}' | env PATH=/tmp/g/bin HOME=/tmp/g/home \ | |
| TMPDIR=/tmp/g/tmp CLAUDE_PROJECT_DIR=/tmp/g/repo bash /tmp/g/nojq.sh) | |
| echo "$out" | jq -e '.decision=="block"' >/dev/null || { echo "did not block without jq"; exit 1; } | |
| echo "ok blocks with no jq reachable" | |
| - name: The session hook injects the routing block on Linux | |
| run: | | |
| set -e | |
| n=$(printf '{"hook_event_name":"SessionStart"}' | bash claude/hooks/inject-session-context.sh | wc -c) | |
| [ "$n" -gt 2000 ] || { echo "session hook injected only $n bytes"; exit 1; } | |
| echo "ok session hook injected $n bytes" | |
| - name: uninstall.sh --dry-run | |
| run: ./uninstall.sh --dry-run | |
| # macOS is the platform this repo calls primary, and until now CI never touched it. Every | |
| # macOS-only assumption — BSD sed and find, `shasum` instead of `sha256sum`, `stat -f` — was | |
| # only ever exercised on the author's laptop, which is the definition of works-on-my-machine. | |
| install-macos: | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install matrix on macOS | |
| run: ./tests/install-matrix.sh | |
| - name: Run the verification gate | |
| run: ./.claude/verify.sh | |
| # Alpine, because BusyBox is not GNU. Its sed, grep, find and stat take different flags, and | |
| # there is no bash until you install it — which is exactly the shape of a slim container | |
| # image. Nothing else in this workflow would catch a GNU-ism. | |
| install-alpine: | |
| runs-on: ubuntu-latest | |
| container: alpine:latest | |
| steps: | |
| - name: Tools a slim image does not ship | |
| run: apk add --no-cache bash git jq curl findutils coreutils grep sed nodejs npm | |
| - uses: actions/checkout@v4 | |
| # The container runs as root against a checkout owned by another uid, so git refuses to | |
| # operate on it at all. That is a property of running in a container, not of this repo, | |
| # but it fails loudly and confusingly: overlay.sh dies mid-run and check 17 reports the | |
| # overlay as broken. Declare the checkout safe so the gate measures the repo rather than | |
| # the runner's file ownership. | |
| - name: Let git operate on a checkout root does not own | |
| run: git config --global --add safe.directory "$GITHUB_WORKSPACE" | |
| - name: What this container actually has | |
| run: | | |
| bash --version | head -1 | |
| for t in jq git sed find cp chmod mktemp sha256sum shasum; do | |
| printf '%-12s %s\n' "$t" "$(command -v $t || echo MISSING)" | |
| done | |
| - name: Install matrix on Alpine | |
| run: bash ./tests/install-matrix.sh | |
| - name: Run the verification gate | |
| run: bash ./.claude/verify.sh |