From 4bfa0cbbc046552e6e211c6911cb3341d8a7c1f6 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 20 Sep 2026 12:23:42 -0400 Subject: [PATCH] fix(plugin): stop CI installing the jq the hooks no longer use (#4107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three loose ends from a49e7ed, all found by CI or by re-reading it. TWO TESTS PINNED THE jq SYNTAX, not the word `jq`, so grepping the suite for the tool name missed them: test_write_path_trigger asserted the literal filter strings `(.note_ids // []) - (.sync_note_ids // [])` and `(.rule_ids // [])[]?` appear in the hook. Re-spelled with the readers that replaced them. The claim each makes is unchanged — that the reuse and sync channels keep their own state file, their own query parameter and their own write-back. CI STILL INSTALLED jq, TWICE. The "Install jq for hook execution tests" step existed because those tests skipped rather than failed without it; the Plugin hooks job installed it alongside shellcheck for the same reason. Removing a dependency from the product and leaving CI to install it anyway means the smoke tests never run under the condition they exist to assert. Both gone — the hook tests now run on a bare image. `awk 'END { print $0 }'` IS UNDEFINED IN POSIX. gawk retains the last record in END and mawk does too, but it is not guaranteed, and reaching for a non-portable idiom inside the change whose entire purpose is portability is the same mistake one layer down. Now `{ last = $0 } END { if (NR) print last }`. Verified it still selects the same start line on a real transcript. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy --- .forgejo/workflows/ci.yml | 27 +++++++++++++-------------- plugin/.claude-plugin/plugin.json | 2 +- plugin/hooks/scribe_report_check.sh | 2 +- tests/test_write_path_trigger.py | 7 ++++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 3d11996..d25cdc0 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -111,7 +111,7 @@ jobs: # Guards the one part of this repo that ships to users without a build step. # See scripts/check_plugin.py for what it checks and, as importantly, what it - # can't check yet (shellcheck and jq are absent from ci-python). + # can't check yet (shellcheck is absent from ci-python). plugin: name: Plugin hooks if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') @@ -133,12 +133,15 @@ jobs: # the only consumer today. Promotion into ci-python is filed as an issue # on CI-runner rather than assumed here. # - # jq is not optional for the smoke test: every hook exits at line 1 - # without it, so the check would pass while exercising nothing. + # jq WAS installed here too, because every hook exited at line 1 without + # it and the smoke test would have passed while exercising nothing. The + # hooks need no jq since #4107 — they use only what POSIX guarantees — so + # the smoke test now runs on a bare image, which is the condition it is + # actually meant to be asserting. - name: Install shell tooling run: | apt-get update -qq - apt-get install -y -qq --no-install-recommends jq shellcheck + apt-get install -y -qq --no-install-recommends shellcheck # On main the comparison would be against itself, so only the syntax and # pattern checks mean anything there. @@ -223,16 +226,12 @@ jobs: UV_PROJECT_ENVIRONMENT: /opt/venv run: uv sync --locked --extra dev - # The hook-EXECUTION tests (test_write_path_trigger's nudge pair) run the - # real bash hook, which exits silently without jq — and those tests skip - # rather than fail when it's absent, so without this step they would - # quietly never be verified anywhere (ci-python ships without jq; same - # install the Plugin hooks job does). - - name: Install jq for hook execution tests - run: | - apt-get update -qq - apt-get install -y -qq --no-install-recommends jq - + # An "Install jq for hook execution tests" step stood here. The hook + # EXECUTION tests run the real bash hook, which exited silently without + # jq and skipped rather than failed, so the step existed to stop them + # being silently unverified. Since #4107 the hooks need no jq, so the + # step is gone and those tests run against a bare image — and the point + # of removing a dependency is lost if CI keeps installing it anyway. - name: Run tests # Integration tests (real Postgres) run in the `integration` job below. run: /opt/venv/bin/python -m pytest tests/ -q -m "not integration" diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 15ac240..07268f1 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "scribe", "description": "Scribe for Claude Code: connects the scribe MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Scribe skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting), and syncs your saved Scribe Processes as skills (/scribe:sync).", - "version": "2026.09.20.1620", + "version": "2026.09.20.1623", "author": { "name": "Bryan Van Deusen" }, diff --git a/plugin/hooks/scribe_report_check.sh b/plugin/hooks/scribe_report_check.sh index d8311f4..90ce991 100644 --- a/plugin/hooks/scribe_report_check.sh +++ b/plugin/hooks/scribe_report_check.sh @@ -119,7 +119,7 @@ turn_facts() { window | tail -n +"${1:-1}" | scribe_json_flat_lines \ # slow way — correct, and slow, which is the right way round for a check that # can block a stop. start=$(window | grep -n -F '"message":{"role":"user","content":"' 2>/dev/null \ - | cut -d: -f1 | awk 'END { if (NR) print $0 }') + | cut -d: -f1 | awk '{ last = $0 } END { if (NR) print last }') case "$start" in ''|*[!0-9]*) start=1 ;; esac facts=$(turn_facts "$start") diff --git a/tests/test_write_path_trigger.py b/tests/test_write_path_trigger.py index 53d8359..e8d488a 100644 --- a/tests/test_write_path_trigger.py +++ b/tests/test_write_path_trigger.py @@ -1091,8 +1091,9 @@ def test_hook_keeps_sync_and_reuse_dedup_apart(): assert ".sync.ids" in src # its own state file assert "exclude_sync_ids=" in src # its own query channel # The reuse file must NOT swallow sync ids — the write-back subtracts them. - assert "(.note_ids // []) - (.sync_note_ids // [])" in src - assert "(.sync_note_ids // [])[]?" in src + # Spelled with the readers that replaced jq (#4107); the claim is unchanged. + assert "scribe_json_list_minus \"$body_flat\" '.note_ids' '.sync_note_ids'" in src + assert "scribe_json_list \"$body_flat\" '.sync_note_ids'" in src def _hook_runtime_env(): @@ -1668,7 +1669,7 @@ def test_hook_keeps_the_rule_channel_apart_from_the_other_three(): src = HOOK.read_text() assert ".rules.ids" in src # its own state file assert "exclude_rule_ids=" in src # its own query channel - assert "(.rule_ids // [])[]?" in src # its own write-back + assert "scribe_json_list \"$body_flat\" '.rule_ids'" in src # its own write-back # And it rides the same request as the rest, not a second round trip. assert "${rule_exclude_q}" in src