Skip to content

Commit 6594bf5

Browse files
kotakanbeclaudegithub-actions[bot]
authored
chore(skills): add consistency-auditor agent + readme-walkthrough hook for Phase A (#382)
* chore(skills): port consistency-auditor + readme-walkthrough hook from vuls-reach Back-port of the Phase A claim-consistency layer from vuls-reach PR #25 (Layer B+C). Adds a 6th conditional Phase A reviewer that catches the "narrative drift" failure mode where the same factual claim (version range, package name, license expression, command flag, manifest header) appears in multiple files within a single PR with different values — the failure shape that drives multi-round Copilot ↔ Claude review cascades on doc-heavy PRs. ## What's added - **`.claude/agents/consistency-auditor.md`** — 6th named subagent for Phase A. Detects cross-file fact drift (8 claim classes: version-range, identifier-literal, command-walkthrough, filename-pattern, schema-column, fix-mechanism-narrative, command-flag, license-expression). Pre-reads `.github/instructions/copilot-learned-coding.instructions.md` for the canonical rule set. 5-step procedure: build fact map -> cross-reference within PR -> cross-reference unchanged files -> walkthrough invariant check -> fix-diff content integrity. - **`.claude/hooks/readme-walkthrough.sh`** — pre-push static check. Awk-based scanner walks fenced bash/sh/shell/zsh/console blocks in changed `*README*.md` and `docs/*.md`, flags blocks that mix repo-root-relative paths (`cmd/`, `internal/`, `pkg/`, `examples/`, `bin/`, ...) with `cd <name>` fixture-relative refs. CRLF-safe; non-blocking `additionalContext` output. root_re prefixes are `cmd|internal|pkg|examples|scripts|testdata|third_party|claude-skills|bin|docs|.github|.claude`, bare-file alternation includes `Makefile|go.mod|go.sum|.golangci.yml|uzomuzo|uzomuzo-diet`. - **`.claude/settings.json`** — wires `readme-walkthrough.sh` into the PreToolUse Bash matcher gated on `git push`, after `pre-push-review.sh`. - **`.github/prompts/review-until-clean.prompt.md`** — Phase A becomes **5 or 6 agents** with a pre-filter: Agent 6 (consistency-auditor) only spawns for PRs that touch `*.md` / `*.txt` / `*.tsv` / `*_test.go` / `testdata/**` / `internal/testdata/**` (claim-bearing files). Pure non-test Go PRs save the spawn cost. NEW **Step 1.5** generates a fact map (4-column schema: Class, Fact key, Value, Asserted in) for doc-heavy PRs that consistency-auditor consumes as its baseline. BASE is captured once at Step 1 so Steps 1.5 / 2 / 4 share the same diff anchor. Stop-condition table updated to reflect the configured agent count. ## Adaptations vs. vuls-reach - **Domain-generalized claim classes**: removed vuls-reach-specific examples (FunctionID, snakeyaml-cve-2017-18640) in favor of OSS scanning examples; added `license-expression` class (uzomuzo-oss- relevant) on top of the seven inherited classes. - **Clone-and-cd exception**: when a block contains `git clone` followed by `cd <name>`, the cd is recognized as a repo-root navigation (not fixture-relative). Avoids false positives on the standard "Build from source" walkthrough at README.md:96. - **`./<name>` no longer triggers fixture signal**: only an explicit `cd <name>` changes shell CWD and is therefore the only signal of fixture-relative state. Bare `./<name>` path arguments (`trivy fs ./my-project`, `./uzomuzo scan`) are command arguments, not CWD changes — flagging them produced false positives on every README example that names a path. - **Skip the replay fixture**: vuls-reach ships `internal/testdata/review-cascade/snakeyaml-pr20/` as acceptance criterion. uzomuzo-oss's domain has no equivalent corpus PR cascade to replay; the agent works without the fixture (fact-map mode + cross-reference). A uzomuzo-relevant fixture is a follow-up. ## Why now uzomuzo-oss already has Layer A (the `.github/instructions/` SoT files and `make sync-instructions` mirror generation), but lacks the *active* detectors that catch new drift before Phase B. Layer B+C is the narrowest, most clearly-portable improvement in vuls-reach PR #25; it has no Go-tool dependency and no CI workflow change, so the back-port is contained to four files. Layer A activation (Phase D in the prompt + `internal/devtools/sync-instructions/` Go tool + `sync-instructions-freshness.yml` CI gate) is a separate scope. ## Verification - `bash -n .claude/hooks/readme-walkthrough.sh` passes. - `jq . .claude/settings.json` parses cleanly. - Smoke test against the existing `README.md`, `claude-skills/README.md`, `docs/*.md` produces zero false positives after the fixture_re simplification. - Synthetic mixed-CWD test (`cd vulnerable && mvn package` followed by `tools/...`) is detected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address Copilot review on PR #382 (round 2) Update doc-code drift in `.claude/hooks/readme-walkthrough.sh`: - Header comment (line 4-8): old text described the heuristic as flagging blocks that mix repo-root paths "with `cd <name>` / `./<name>` fixture-relative refs". The implementation no longer treats bare `./<name>` path arguments as fixture-relative — only `cd <name>` is a true CWD change. Updated to `cd <name>` only and added a note that `./<name>` arguments do NOT trigger the signal. - `scan_blocks` comment (line 55): same drift, same fix. - User-facing warning printf (line 124): the warning text shown to developers when the hook fires said "fixture-relative (cd <name>, ./<name>) paths". Updated to "a `cd <name>` fixture-relative shell CWD change" so developers see the actual heuristic. The two `WONT_FIX` Copilot threads on this PR (line 47, line 194) are about `docs/*.md` pathspec recursion. Empirical test (with a fresh git repo containing `docs/top.md`, `docs/adr/foo.md`, `docs/case-studies/bar.md`): $ git ls-files -- 'docs/*.md' docs/adr/README.md docs/adr/foo.md docs/case-studies/bar.md docs/top.md confirms `docs/*.md` matches nested files (git's default pathspec uses fnmatch where `*` matches `/`). The script's existing comment on the DOC_FILES line ("`*` matches any character INCLUDING `/`") is correct; no fix needed. Phase C will reply WONT_FIX with the test evidence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: pre-empt mirror-provenance drift in prompt (PR #382 round 3) The Phase A Step 2 paragraph said `.claude/rules/agents.md` is "hand-curated", but in this repo that file is generated from `.github/instructions/agent-orchestration.instructions.md` (carries the canonical "DO NOT EDIT DIRECTLY" mirror header from `make sync-instructions`). Reframed the paragraph: `consistency-auditor` is invoked by file presence at `.claude/agents/consistency-auditor.md` regardless of any registry, so the generated `agents.md` mirror does not need to list it for the skill to work. The instruction-file SoT update is a follow-up, independent of this skill working. Same correction was made to the catalog port (uzomuzo-catalog PR #227 round 2) where Copilot caught the same drift; applying it here proactively before Copilot's next review pass on the same paragraph. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: instruction-sync compliance and comment-code consistency (PR #382 Phase A) - Convert .claude/agents/consistency-auditor.md to thin delegation shim - Create .github/agents/consistency-auditor.agent.md as canonical source - Fix phantom rule references ("Update Cross-File Facts Atomically", "Documentation & Narrative Consistency") → "Comment-Code Consistency" - Fix "claim triples" → "claim records" (schema is 4-field tuples) - Add sh-session/shell-session to hook header and scan_blocks comments (aligning comments with the fence_open regex that already matches them) - Add "e.g.," qualifier to Agent 6 claim-class list (non-exhaustive) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove redundant gsub mask for bare ./<prefix> paths (PR #382 round 4) The first gsub that stripped `./(repo-root-prefix)/...` tokens was dead code: fixture_re requires a `cd` prefix, so bare `./cmd/uzomuzo` path arguments can never match it. The comment incorrectly claimed these tokens would be "double-counted as fixture-relative". Remove the dead gsub and correct the comment to accurately describe the cd-only masking heuristic. Co-authored-by: Kota Kanbe <kotakanbe@users.noreply.github.com> * fix: correct misleading comment about bare-file gsub rationale (PR #382 round 5) The comment claimed `./go.mod` would match fixture_re, but fixture_re requires a `cd` prefix — bare path arguments can never trigger it. The actual false-positive scenario is `cd uzomuzo` (repo-root directory name matching cd_arg's `[a-z][a-z0-9_./-]*` pattern). Corrected the example and listed all masked tokens (the old comment omitted uzomuzo/uzomuzo-diet). Co-authored-by: Kota Kanbe <kotakanbe@users.noreply.github.com> * fix: narrow clone-and-cd exception to first post-clone cd only (PR #382 round 6) The `saw_clone` flag was never cleared after consuming the post-clone `cd <repo-name>`, so ALL subsequent `cd <single-token>` commands in the same block were masked. A later `cd demo` (real fixture transition) would be silently stripped, hiding a genuine mixed-CWD mismatch. Fix: wrap the gsub in `if (gsub(...))` and clear `saw_clone = 0` after the first post-clone cd is consumed. Later cd commands now participate in normal fixture detection. Co-authored-by: Kota Kanbe <kotakanbe@users.noreply.github.com> * fix: address Copilot doc-code drift on PR #382 (round 7) — BASE/Step 4 Sister fix to uzomuzo-catalog PR #227 round 5. Two related fixes to `.github/prompts/review-until-clean.prompt.md`: 1. **BASE/Step 4 inconsistency**: the Step 1 BASE comment (line 72-75) and the Step 2 paragraph (line 116) both claimed BASE is reused for "Step 4's verification", but Step 4 in this prompt is the build/ vet/test/lint step and does not reference BASE — only Step 1.5 fact-map walk and Step 2 pre-filter use it. Reworded both sites to mention only the steps that actually use BASE. 2. **BASE fallback robustness**: the original `git merge-base HEAD origin/main 2>/dev/null || echo main` silently degrades to a literal `main` string if `origin/main` is unavailable, so on a `--single-branch=feature` clone with no `main` ref the subsequent `git diff "$BASE" HEAD` would error and DOC_TOUCHING (the AGENT_COUNT pre-filter signal) would silently become empty. Added a middle fallback to local `main` merge-base: `... || git merge-base HEAD main 2>/dev/null || echo main`. Copilot's most recent review on a8c5c75 reported "no new comments" (literal Copilot-clean), but the user's `/review-until-clean` re-review experiment on the sister catalog PR surfaced the same inherited drift on prompt.md — fixing here pre-emptively to keep the two ports consistent and to avoid Copilot flipping the catalog-side finding onto oss in a future review pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address Copilot review on PR #382 (round 8) — pathspec comment + repro hint Two doc-code drift fixes in `.claude/hooks/readme-walkthrough.sh`: 1. **Exclusion comment uses short form, code uses long form** (Copilot thread `_AIrD8`): the comment described the testdata exclusions as `:!testdata/**` / `:!internal/testdata/**` while the actual `git diff` line used `:(exclude)testdata/**` / `:(exclude)internal/testdata/**`. Both forms are equivalent (short `:!` vs long `:(exclude)` git pathspec magic), but the inconsistent doc form confuses readers trying to map the comment to the code. Updated the comment to use the long form (matches the code) and noted the equivalence to the short form. 2. **`additionalContext` repro hint missed exclusions** (sister thread on catalog #227 round 6 surfaced this): the user-facing `additionalContext` message suggested `git diff ${BASE} HEAD -- "*README*.md" "docs/*.md"` for reproduction, but the scanner's actual DOC_FILES pathspec also excludes `testdata/**` and `internal/testdata/**`. Reproducers running the suggested command would see different files than the scanner walked. Updated the hint to include the same exclusions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent 3812a21 commit 6594bf5

5 files changed

Lines changed: 432 additions & 6 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
name: consistency-auditor
3+
description: Cross-file narrative consistency auditor. Detects the failure mode where the same factual claim (e.g., version range, package name, license expression, command flag, manifest header, fix-mechanism narrative) appears in multiple files inside one PR with different values.
4+
tools: Read, Grep, Glob, Bash
5+
model: opus
6+
---
7+
8+
# Consistency Auditor
9+
10+
> **Full specification**: See `.github/agents/consistency-auditor.agent.md` for the complete
11+
> claim classes, procedure steps, output format, and approval criteria.
12+
13+
## Quick Reference
14+
15+
- Build a fact map from the diff (4-field tuples: class, key, value, file:line)
16+
- 8 claim classes: version-range, identifier-literal, command-walkthrough, filename-pattern, schema-column, fix-mechanism-narrative, command-flag, license-expression
17+
- Cross-reference within PR, then against unchanged files (within +/-2 directory steps)
18+
- Check walkthrough shell blocks for mixed working-directory signals
19+
- APPROVE (zero drifts), BLOCK (any unresolved finding)
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/usr/bin/env bash
2+
# readme-walkthrough.sh — pre-push static check for README / docs walkthroughs.
3+
#
4+
# Catches the "walkthrough-mismatch" class: a fenced ` ```bash ` /
5+
# ` ```sh ` / ` ```shell ` / ` ```zsh ` / ` ```console ` / ` ```sh-session ` /
6+
# ` ```shell-session ` block in README*.md or
7+
# docs/*.md that mixes repo-root-relative paths (e.g., `cmd/uzomuzo/...`,
8+
# `internal/...`) with a `cd <name>` fixture-relative shell CWD change. The
9+
# block cannot be copy-pasted verbatim from any single CWD, so readers get
10+
# stuck. Note: only `cd <name>` actually changes CWD; bare `./<name>` path
11+
# arguments do NOT trigger the fixture signal (see Step body below for why).
12+
#
13+
# Hook contract: PreToolUse on Bash matcher gated on `git push`. Reads the
14+
# tool input JSON from stdin (same as adr-check.sh / pre-push-review.sh). If
15+
# issues are detected, emits an `additionalContext` JSON line that Claude
16+
# Code surfaces back into the conversation. Non-blocking by design — the
17+
# user can still push if they decide the heuristic is wrong; the next
18+
# /review-until-clean Phase A round will catch real cases via the
19+
# consistency-auditor agent.
20+
set -euo pipefail
21+
22+
CMD=$(node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d||'{}');console.log(j.tool_input?.command||'');}catch(e){console.log('');}});" <&0)
23+
24+
# Trigger on any occurrence of `git push` in the command (whitespace-bounded,
25+
# so `git pushback` does not trigger). Common Bash-tool invocations include:
26+
# `cd repo && git push`
27+
# `env GH_TOKEN=... git push`
28+
# multi-line scripts ending in `git push`
29+
# A whitespace-or-start prefix plus whitespace-or-end suffix is sufficient
30+
# for the heuristic; we never act on the false-positive cases (the hook is
31+
# advisory `additionalContext`-only).
32+
if ! echo "$CMD" | grep -qE '(^|[[:space:]]|;|&|\|)git[[:space:]]+push([[:space:]]|;|&|\||$)'; then
33+
exit 0
34+
fi
35+
36+
BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null || echo "")
37+
if [ -z "$BASE" ]; then
38+
exit 0
39+
fi
40+
41+
# Pathspecs use git's default fnmatch where `*` matches any character INCLUDING
42+
# `/` — so `*README*.md` matches READMEs at any depth, and `docs/*.md`
43+
# recurses through `docs/<sub>/`, etc.
44+
#
45+
# `:(exclude)testdata/**` / `:(exclude)internal/testdata/**` exclusion
46+
# (long-form git pathspec magic; equivalent to the short `:!` prefix):
47+
# testdata fixtures may DELIBERATELY contain mixed-CWD shell blocks as
48+
# acceptance data for the consistency-auditor agent or other test
49+
# scenarios. Without the exclusion,
50+
# every edit to those fixtures would produce a pre-push warning even though
51+
# the mismatch is the whole point of the fixture.
52+
DOC_FILES=$(git diff --name-only "$BASE" HEAD -- '*README*.md' 'docs/*.md' ':(exclude)testdata/**' ':(exclude)internal/testdata/**' 2>/dev/null | sort -u || true)
53+
if [ -z "$DOC_FILES" ]; then
54+
exit 0
55+
fi
56+
57+
# scan_blocks <file>: walks the file inside one awk program (no fold/unfold
58+
# round-trip), tracks fenced bash/sh/shell/zsh/console/sh-session/shell-session blocks, and emits
59+
# "<file>:<start-line>: <reason>" for each block whose lines mix repo-root-
60+
# relative paths with a `cd <name>` fixture-relative shell CWD change. Only
61+
# `cd <name>` is a true CWD change; bare `./<name>` path arguments are not
62+
# treated as fixture-relative (see fixture_re comment in BEGIN).
63+
#
64+
# Awk is the right tool here because (a) it handles the line-by-line state
65+
# machine cleanly, (b) it avoids the bash `||`-as-newline-separator hack
66+
# (which would corrupt blocks containing literal `||` shell logical-or), and
67+
# (c) it strips the trailing CR for CRLF line endings via gsub.
68+
scan_blocks() {
69+
local file="$1"
70+
awk -v file="$file" '
71+
function reset_block() {
72+
in_block = 0
73+
start_line = 0
74+
saw_root = 0
75+
saw_fixture = 0
76+
saw_clone = 0
77+
}
78+
79+
BEGIN {
80+
reset_block()
81+
# POSIX character classes: ERE does not define \s. Keep bracket
82+
# expressions explicit so the script works on both gawk and BSD awk.
83+
# Repo-root anchors include `./bin/` so commands like `./bin/uzomuzo`
84+
# are classified as repo-root-relative.
85+
# Keep this prefix list aligned with the gsub mask below — when one
86+
# treats a path as repo-root, the other must too (otherwise a mixed
87+
# block can have its repo-root signal stripped from fixture_re without
88+
# firing root_re, and the mismatch goes undetected).
89+
# Repo-root anchors: directory prefixes (cmd/, internal/, pkg/, etc.)
90+
# AND bare top-level repo files commonly referenced from walkthroughs
91+
# (Makefile, go.mod, go.sum, .golangci.yml). Without the bare-file
92+
# alternatives, a block that says `cd demo && cat go.mod` would have
93+
# saw_root=0 and slip through even though the block requires repo-root
94+
# CWD to find go.mod.
95+
root_re = "(^|[[:space:]])(\\./)?(cmd|internal|pkg|examples|scripts|testdata|third_party|claude-skills|bin|docs|\\.github|\\.claude)/"
96+
root_re = root_re "|(^|[[:space:]])(\\./)?(Makefile|go\\.mod|go\\.sum|\\.golangci\\.yml|uzomuzo|uzomuzo-diet)([[:space:]]|$)"
97+
# Catch `cd <name>` in five common shapes: `cd foo`, `cd foo/`,
98+
# `cd ./foo`, `cd ./foo/`, and multi-segment forms like
99+
# `cd internal/corpus/sample`. The cd target is what actually changes
100+
# the shell CWD; bare `./<name>` path arguments (e.g.,
101+
# `trivy fs ./my-project`, `./uzomuzo scan`) do NOT change CWD and
102+
# therefore must not trigger the fixture signal — flagging them
103+
# produces false positives on every README example that names a path.
104+
cd_arg = "(\\./)?[a-z][a-z0-9_./-]*/?"
105+
fixture_re = "(^|[[:space:]])cd[[:space:]]+" cd_arg "([[:space:]]|$)"
106+
# Fence opener: tolerates optional whitespace plus either (a) a single-
107+
# token language info-string suffix like `bash {.line-numbers}` or
108+
# `bash title=...` (whitespace then arbitrary content), or (b) common
109+
# session-flavored language tags like `sh-session` / `shell-session`
110+
# (treated as single tokens by the GitHub Markdown syntax highlighter).
111+
fence_open = "^```[[:space:]]*(bash|sh|shell|zsh|console|sh-session|shell-session)([[:space:]].*)?$"
112+
fence_close = "^```[[:space:]]*$"
113+
}
114+
115+
{
116+
# Strip CR so CRLF-line-ending docs are handled identically to LF docs.
117+
gsub(/\r$/, "")
118+
}
119+
120+
!in_block && match($0, fence_open) {
121+
in_block = 1
122+
start_line = NR
123+
saw_root = 0
124+
saw_fixture = 0
125+
saw_clone = 0
126+
next
127+
}
128+
129+
in_block && match($0, fence_close) {
130+
if (saw_root && saw_fixture) {
131+
printf "%s:%d: shell block mixes repo-root-relative paths (e.g., cmd/, internal/, pkg/) with a `cd <name>` fixture-relative shell CWD change — readers cannot copy-paste from a single CWD\n", file, start_line
132+
}
133+
reset_block()
134+
next
135+
}
136+
137+
in_block {
138+
if (match($0, root_re)) saw_root = 1
139+
# `git clone <url>` followed by `cd <repo-name>` is the canonical
140+
# "from-scratch install" pattern: the cd target becomes the repo root,
141+
# so any subsequent repo-root-relative paths are coherent. Detect the
142+
# clone and skip the cd-after-clone from fixture_re below.
143+
if (match($0, /(^|[[:space:]])git[[:space:]]+clone([[:space:]]|$)/)) saw_clone = 1
144+
# Mask any `cd <repo-root-prefix>...` token before fixture_re
145+
# matches. Without this, `cd internal/corpus/...` (a perfectly
146+
# valid repo-root walkthrough) fires fixture_re via the `cd ...`
147+
# shape even though the cd target IS itself a repo-root path.
148+
# Bare `./<prefix>/...` path arguments (without `cd`) need no
149+
# masking because fixture_re requires a `cd` prefix — they
150+
# cannot match fixture_re regardless.
151+
# The mask strips repo-root cd targets from a per-line copy
152+
# before fixture_re inspects it, so genuine fixture-relative
153+
# tokens (`cd vulnerable`) still match while repo-root cd
154+
# targets are excluded.
155+
line_for_fixture = $0
156+
gsub(/(^|[[:space:]])cd[[:space:]]+(\.\/)?(cmd|internal|pkg|examples|scripts|testdata|third_party|claude-skills|bin|docs|\.github|\.claude)\/[^[:space:]]*/, " ", line_for_fixture)
157+
# Also strip bare top-level repo-root file tokens (Makefile, go.mod,
158+
# go.sum, .golangci.yml, uzomuzo, uzomuzo-diet). The first gsub above
159+
# only strips `cd <dir-prefix>/...` (note the trailing `/`), so bare
160+
# names without a path separator slip through. Without this mask, a
161+
# line like `cd uzomuzo && go build ./cmd/uzomuzo` would have
162+
# `cd uzomuzo` match fixture_re (since `uzomuzo` starts with [a-z]
163+
# and satisfies cd_arg), while `cmd/` matches root_re — producing a
164+
# false-positive mixed-CWD flag even though `cd uzomuzo` is navigating
165+
# into the repo root (after a clone), not a fixture subdirectory.
166+
# Keep this list in lockstep with the bare-file alternation in
167+
# root_re above.
168+
gsub(/(^|[[:space:]])(\.\/)?(Makefile|go\.mod|go\.sum|\.golangci\.yml|uzomuzo|uzomuzo-diet)([[:space:]]|$)/, " ", line_for_fixture)
169+
# If `git clone` appeared earlier in this block, strip the FIRST
170+
# post-clone `cd <single-token>` so a benign clone-and-cd block like:
171+
# git clone https://github.com/foo/bar
172+
# cd bar
173+
# go build ./cmd/bar
174+
# is not flagged. The mask only strips a single-segment cd target
175+
# (no slashes), which is the shape that matches the working
176+
# directory of a freshly-cloned repo. After consuming the post-clone
177+
# cd, clear `saw_clone` so later cd commands (`cd demo`, `cd fixture`)
178+
# still participate in mismatch detection — otherwise the exception
179+
# applies too broadly and hides real fixture transitions.
180+
if (saw_clone) {
181+
if (gsub(/(^|[[:space:]])cd[[:space:]]+(\.\/)?[a-z][a-z0-9_-]*\/?([[:space:]]|$)/, " ", line_for_fixture)) {
182+
saw_clone = 0
183+
}
184+
}
185+
if (match(line_for_fixture, fixture_re)) saw_fixture = 1
186+
}
187+
' "$file"
188+
}
189+
190+
ISSUES=()
191+
while IFS= read -r doc; do
192+
[ -z "$doc" ] && continue
193+
# Skip files deleted in the diff (DOC_FILES comes from `git diff --name-only`,
194+
# which lists deletions too). awk on a missing file would error and, under
195+
# `set -e`, would kill the hook even though it is intended to be non-blocking.
196+
[ ! -f "$doc" ] && continue
197+
while IFS= read -r line; do
198+
[ -z "$line" ] && continue
199+
ISSUES+=("$line")
200+
done < <(scan_blocks "$doc")
201+
done <<< "$DOC_FILES"
202+
203+
if [ ${#ISSUES[@]} -gt 0 ]; then
204+
BULLET_LIST=$(printf '\\n- %s' "${ISSUES[@]}")
205+
MSG="PRE-PUSH README WALKTHROUGH: Working-directory mismatch detected in fenced shell blocks. Either prefix the block with an explicit \\\"Run from <CWD>\\\" hint and rewrite all paths to that CWD, or split into two blocks each consistent with one CWD.${BULLET_LIST}\\n\\nThis is the walkthrough-mismatch class tracked by the consistency-auditor agent (.claude/agents/consistency-auditor.md). Run 'git diff ${BASE} HEAD -- \\\"*README*.md\\\" \\\"docs/*.md\\\" \\\":(exclude)testdata/**\\\" \\\":(exclude)internal/testdata/**\\\"' to reproduce the same file set the scanner walked."
206+
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"${MSG}\"}}"
207+
fi

.claude/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@
3838
"command": "bash .claude/hooks/pre-push-review.sh",
3939
"timeout": 30,
4040
"statusMessage": "Self-reviewing for Copilot issues..."
41+
},
42+
{
43+
"type": "command",
44+
"command": "bash .claude/hooks/readme-walkthrough.sh",
45+
"timeout": 15,
46+
"statusMessage": "Checking README walkthroughs for working-directory mismatches..."
4147
}
4248
]
4349
}

0 commit comments

Comments
 (0)