feat(toolkit): verify-branch-state skill + /verify + warn-mode hook (DOJ-4872) — v1.31.0 - #44
Conversation
…e hook (DOJ-4872) — v1.31.0
Ships the plugin-layer (Level 1) extension of the working-tree-vs-ref
discipline rule. DOJ-4868 shipped the repo-layer (Level 2) enforcement in
dojo-os; this release makes the same rule travel with the toolkit to every
repo it is installed in.
New artifacts:
- skills/verify-branch-state/SKILL.md — auto-loading skill, tuned description
triggers on "verify against develop", "does X exist on <branch>", "what's
on origin/...", "did this land on main", "re-validate the subagent". Body
covers the rule, canonical ref-explicit commands (git fetch + git show /
ls-tree / grep / diff against origin/<ref>), anti-patterns (ls, grep -rn
src/, find, git status), pre-flight checklist, multi-worktree context, and
the 2026-06-03 DOJ-4851/4863/4864 origin incident. Cites memory
feedback_working_tree_is_not_truth.
- commands/verify.md — slash command /verify <ref> <path-or-pattern>. Runs
git fetch origin <ref> --quiet then git ls-tree origin/<ref> -- <path>
for paths or git grep <pattern> origin/<ref> -- src/ for patterns. Emits
a single verdict line citing the ref's short SHA in the shape
"origin/<ref> @ <sha7> — <path-or-pattern> <PRESENT|NOT PRESENT|N MATCHES>"
so the claim can be re-checked. Handles ref-not-found, shell
metacharacters in patterns, and squash-merge cache staleness (always
re-fetches).
- hooks/pre-tool-use-claim-verification.sh — warn-mode PreToolUse hook,
repo-agnostic, no per-repo manifest. Detects bare-FS verification ops
combined with intent keywords; prints stderr warning with local HEAD ref
+ SHA, a suggested ref-explicit replacement, and pointers to the skill +
slash command. Always exit 0. Defensive set +e. Uses cd -P + pwd -P from
day 1 (DOJ-4868 PR #2604 lesson). Trailing-newline-safe while read.
Honors CLAUDE_DISABLE_PLUGIN_HOOKS=1. Shellcheck clean. Wired into
hooks/hooks.json under the Bash PreToolUse matcher with a 3s timeout.
Version bump 1.30.0 -> 1.31.0 across .claude-plugin/marketplace.json
(top-level + plugin version) + .claude-plugin/plugin.json + package.json +
README.md Version header. CHANGELOG entry under [1.31.0] - 2026-06-03 with
Added / Changed / Why sections + reference link at bottom. README "Start
here" grows a peer entry point ("Also start here: verify-branch-state +
/verify") between the domain-driven-advisor section and "What's Inside".
Commands table 29 -> 30 (adds /verify); Skills table 10 -> 11 (adds
verify-branch-state). Marketplace plugin description pair-mentions the new
skill.
Smoke tests pass:
- bash hooks/pre-tool-use-claim-verification.sh "ls src/foo.tsx" -> warn
- bash hooks/pre-tool-use-claim-verification.sh "echo hello" -> silent
- shellcheck hooks/pre-tool-use-claim-verification.sh -> 0 issues
- jq -r .version on all four manifests -> 1.31.0 (agree)
AC9 note (cohesive-PR exception): 9 files instead of the soft ≤8 target
because the hooks.json registration is required to make the new hook
non-dead. The new skill + command + hook are a single cohesive surface
(working-tree-vs-ref discipline) and the version+CHANGELOG+README updates
are the standard release-cut shape, mirroring the v1.30.0 commit shape.
Related: DOJ-4868 (dojo-os Level-2 sibling), v1.30.0 release (today),
memory feedback_working_tree_is_not_truth.
Created by Claude Code on behalf of @lapc506.
|
@dojo-code-reviewer review |
There was a problem hiding this comment.
✅ Approved
Approved — 0 blockers, 2 P2, 2 P3. Confidence: 3/5.
🟡 P2 — Major
-
commands/verify.md:42— 🟡 P2 (major) — The slash command currently assumes the user always passes a branch name that needs to be prefixed with 'origin/'. However, if a user passes a remote-tracking branch like 'origin/develop', a tag like 'v1.31.0', or a commit SHA, this leads to failures like 'origin/origin/develop' or 'origin/v1.31.0' during fetch, rev-parse, and ls-tree/grep. Additionally, the hardcoded '-C "$REPO"' directory switches can fail if the environment hasn't defined the '$REPO' variable. Normalize the reference explicitly first to handle all formats seamlessly, and execute directly within the current workspace. -
hooks/pre-tool-use-claim-verification.sh:116— 🟡 P2 (major) — The regex matches bare-filesystem operations followed by a space, which means it will fail to detect verification commands that appear at the end of a line or command string without a trailing space (for example, 'ls' alone or the end of a chain like 'cd src && ls'). Use '([[:space:]]|$)' to ensure the hook reliably triggers in these scenarios.
if printf '%s' "$cmd" | grep -qE '(^|[;&|][[:space:]]*)(ls|find|cat|head)([[:space:]]|$)'; then
🔵 P3 — Minor
hooks/pre-tool-use-claim-verification.sh:162— 🔵 P3 (minor) — To ensure that the suggested replacement helper is chosen correctly when 'ls' is used at the end of a command string without trailing space, replace 'ls[[:space:]]' with 'ls([[:space:]]|$)'.
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|][[:space:]]*)ls([[:space:]]|$)'; then
hooks/pre-tool-use-claim-verification.sh:181— 🔵 P3 (minor) — To ensure that the suggested replacement helper is chosen correctly when 'find' is used at the end of a command string without trailing space, replace 'find[[:space:]]' with 'find([[:space:]]|$)'.
elif printf '%s' "$COMMAND" | grep -qE '(^|[;&|][[:space:]]*)find([[:space:]]|$)'; then
Total findings: 2 compliance, 2 business context (4 total)
ℹ️ Inline anchoring failed for the comments below; they are listed here instead.
commands/verify.md:42— 🟡 P2 (major) — The slash command currently assumes the user always passes a branch name that needs to be prefixed with 'origin/'. However, if a user passes a remote-tracking branch like 'origin/develop', a tag like 'v1.31.0', or a commit SHA, this leads to failures like 'origin/origin/develop' or 'origin/v1.31.0' during fetch, rev-parse, and ls-tree/grep. Additionally, the hardcoded '-C "$REPO"' directory switches can fail if the environment hasn't defined the '$REPO' variable. Normalize the reference explicitly first to handle all formats seamlessly, and execute directly within the current workspace.hooks/pre-tool-use-claim-verification.sh:116— 🟡 P2 (major) — The regex matches bare-filesystem operations followed by a space, which means it will fail to detect verification commands that appear at the end of a line or command string without a trailing space (for example, 'ls' alone or the end of a chain like 'cd src && ls'). Use '([[:space:]]|$)' to ensure the hook reliably triggers in these scenarios.
if printf '%s' "$cmd" | grep -qE '(^|[;&|][[:space:]]*)(ls|find|cat|head)([[:space:]]|$)'; then
hooks/pre-tool-use-claim-verification.sh:162— 🔵 P3 (minor) — To ensure that the suggested replacement helper is chosen correctly when 'ls' is used at the end of a command string without trailing space, replace 'ls[[:space:]]' with 'ls([[:space:]]|$)'.
if printf '%s' "$COMMAND" | grep -qE '(^|[;&|][[:space:]]*)ls([[:space:]]|$)'; then
hooks/pre-tool-use-claim-verification.sh:181— 🔵 P3 (minor) — To ensure that the suggested replacement helper is chosen correctly when 'find' is used at the end of a command string without trailing space, replace 'find[[:space:]]' with 'find([[:space:]]|$)'.
elif printf '%s' "$COMMAND" | grep -qE '(^|[;&|][[:space:]]*)find([[:space:]]|$)'; then
Bot review at 3/5 confidence flagged 2 P2 + 2 P3. All four addressed:
P2 — commands/verify.md ref normalization
- The behavior section assumed every <ref> arg was a bare branch name
and unconditionally prefixed `origin/`, breaking `origin/develop`
(→ `origin/origin/develop`), tags (`v1.31.0` → `origin/v1.31.0`),
and SHAs. Added a normalization step at the top of the pipeline:
* `refs/tags/<tag>` for tag inputs (fetched via `git fetch origin
tag <tag> --quiet`)
* bare SHA for already-local commits (skips fetch entirely;
commits are immutable)
* `origin/<branch>` left as-is when user passes `origin/...`
* `origin/<branch>` only synthesized for bare branch input
- Dropped the `-C "$REPO"` indirection — the slash command's cwd is
the repo root, no need for the hardcoded variable that would fail
when unset.
- Updated the verdict-line shape to `<resolved-ref> @ <sha7>` so the
citation reflects whatever normalization produced (tag refs, SHAs,
branch refs all render correctly). Added tag + SHA examples.
- Expanded the Edge cases section: `origin/<branch>` input, tag input,
commit SHA input, ref not found.
P2 — hooks/pre-tool-use-claim-verification.sh line 116
- The bare-FS detector regex `(ls|find|cat|head)[[:space:]]` failed
to match the command-as-last-token cases like a bare `ls` invocation
or `cd src && ls` (no trailing whitespace). Switched the trailing
class to `([[:space:]]|$)` so end-of-string also matches.
P3 — hooks/pre-tool-use-claim-verification.sh line 162
- Same fix in the suggester branch for `ls`: `ls[[:space:]]` →
`ls([[:space:]]|$)` so the ls-specific replacement text is emitted
even when the command ends in `ls`.
P3 — hooks/pre-tool-use-claim-verification.sh line 181
- Same fix in the suggester branch for `find`: `find[[:space:]]` →
`find([[:space:]]|$)`.
Verification:
- shellcheck hooks/pre-tool-use-claim-verification.sh → 0 issues
- bash hooks/pre-tool-use-claim-verification.sh "ls" → warn (new)
- bash hooks/pre-tool-use-claim-verification.sh "cd src && ls" → warn (new)
- bash hooks/pre-tool-use-claim-verification.sh "ls src/foo" → warn (regression)
- bash hooks/pre-tool-use-claim-verification.sh "echo hello" → silent (regression)
- bash hooks/pre-tool-use-claim-verification.sh "find" → warn (new)
Created by Claude Code on behalf of @lapc506.
|
@dojo-code-reviewer review Addressed all 4 findings from prior review (3/5 confidence):
Smoke tests + shellcheck still green. Fix commit: 31f10c9. |
There was a problem hiding this comment.
✅ Approved
Approved — 0 blockers, 2 P3. Confidence: 4/5.
🔵 P3 — Minor
hooks/pre-tool-use-claim-verification.sh:145— 🔵 P3 (minor) — Simplify and make the path argument extraction forlscommands more robust. The currentsedregular expression assumes the flags always have a trailing space, which fails to strip flags if they are at the end of the command (e.g.,ls -la), resulting inpath_argbeing extracted as-la. Usingawkto loop through the tokens and extract the first non-option token afterlsis cleaner, shorter, and correctly handles trailing options or extra whitespace.
path_arg="$(printf '%s' "$COMMAND" \
| awk '{for(i=1;i<=NF;i++) if ($i == "ls") {for(j=i+1;j<=NF;j++) if ($j !~ /^-/) {print $j; exit}}}')"
hooks/pre-tool-use-claim-verification.sh:158— 🔵 P3 (minor) — Fix thegreppattern extraction when multiple option flags are specified separately. In the current implementation, if a user runsgrep -i -r "Symbol" src/, theawkblock matches-iand immediately extracts the next token$(i+1)(which is-r) as the pattern. By utilizing a nested loop inawkthat skips any subsequent option tokens starting with-, you can correctly isolate the actual search pattern.
pattern_arg="$(printf '%s' "$COMMAND" \
| awk '{for(i=1;i<=NF;i++) if ($i ~ /^-[rRnliN]+$/) {for(j=i+1;j<=NF;j++) if ($j !~ /^-/) {print $j; exit}}}' \
| tr -d '\"')"
Total findings: 2 business context (2 total)
ℹ️ Inline anchoring failed for the comments below; they are listed here instead.
hooks/pre-tool-use-claim-verification.sh:145— 🔵 P3 (minor) — Simplify and make the path argument extraction forlscommands more robust. The currentsedregular expression assumes the flags always have a trailing space, which fails to strip flags if they are at the end of the command (e.g.,ls -la), resulting inpath_argbeing extracted as-la. Usingawkto loop through the tokens and extract the first non-option token afterlsis cleaner, shorter, and correctly handles trailing options or extra whitespace.
path_arg="$(printf '%s' "$COMMAND" \
| awk '{for(i=1;i<=NF;i++) if ($i == "ls") {for(j=i+1;j<=NF;j++) if ($j !~ /^-/) {print $j; exit}}}')"
hooks/pre-tool-use-claim-verification.sh:158— 🔵 P3 (minor) — Fix thegreppattern extraction when multiple option flags are specified separately. In the current implementation, if a user runsgrep -i -r "Symbol" src/, theawkblock matches-iand immediately extracts the next token$(i+1)(which is-r) as the pattern. By utilizing a nested loop inawkthat skips any subsequent option tokens starting with-, you can correctly isolate the actual search pattern.
pattern_arg="$(printf '%s' "$COMMAND" \
| awk '{for(i=1;i<=NF;i++) if ($i ~ /^-[rRnliN]+$/) {for(j=i+1;j<=NF;j++) if ($j !~ /^-/) {print $j; exit}}}' \
| tr -d '\"')"
…n hook (DOJ-4872)
Round-2 bot review at 4/5 confidence flagged 2 P3 polish items on the
suggester's argument extraction. Both addressed; suggestions applied
verbatim with explanatory comments.
P3 — hooks/pre-tool-use-claim-verification.sh:145 (ls path extraction)
- Old: sed prefix-strip + awk first-token, which fails on commands
like `ls -la` (no path arg, trailing flags) — sed regex `-flags`
requires a trailing space, so the flags leak through and `-la` ends
up as the "path arg" in the suggested replacement.
- New: awk-only nested loop that locates the literal `ls` token,
then walks forward skipping any token starting with `-`, and
emits the first non-flag token. Empty when only flags are present
(correct), so the suggester falls through to the `<path>` placeholder.
P3 — hooks/pre-tool-use-claim-verification.sh:158 (grep pattern extraction)
- Old: awk fallback matched the first `-FLAGS` token and emitted the
immediately following token, which on `grep -i -r "Symbol" src/`
is `-r` — wrong.
- New: nested awk loop that, after finding the flag token, walks
forward skipping additional flag tokens before emitting the first
non-flag one. Correctly isolates `Symbol` for `grep -i -r "Symbol"`,
still correct for the single-flag-block form `grep -rn "Symbol"`.
Verification:
- shellcheck → 0 issues
- "ls -la" → suggester emits <path> placeholder (was: `-la`)
- "ls src/foo.tsx" → suggester emits `src/foo.tsx` (regression)
- "grep -i -r \"Symbol\" src/" → emits "Symbol" (was: `-r`)
- "grep -rn \"Symbol\" src/" → emits "Symbol" (regression)
- "echo hello" → silent (regression)
Created by Claude Code on behalf of @lapc506.
|
@dojo-code-reviewer review Round-2 feedback (4/5) addressed — both P3 items applied verbatim with explanatory comments. Fix commit: $(git rev-parse --short HEAD).
Smoke tests + shellcheck green. |
There was a problem hiding this comment.
✅ Approved
Approved — 0 blockers, 1 P3. Confidence: 4/5.
🔵 P3 — Minor
hooks/pre-tool-use-claim-verification.sh:185— 🔵 P3 (minor) — Strip single quotes as well as double quotes in the fallback pattern extractor to handle single-quoted search patterns (e.g.grep -rn 'Symbol').
| tr -d "\"'")"
Total findings: 1 business context (1 total)
ℹ️ Inline anchoring failed for the comments below; they are listed here instead.
hooks/pre-tool-use-claim-verification.sh:185— 🔵 P3 (minor) — Strip single quotes as well as double quotes in the fallback pattern extractor to handle single-quoted search patterns (e.g.grep -rn 'Symbol').
| tr -d "\"'")"
…n hook (DOJ-4872) Round-3 bot review at 4/5 confidence flagged 1 P3: the pattern extractor only handled double-quoted forms, so `grep -rn 'Symbol' src/` would fall through to the awk fallback and (until the awk fallback also stripped single quotes) emit `'Symbol'` with the quotes attached. Applied: - Added a second extractor branch that mirrors the double-quote form but for single-quoted patterns. Tries double-quote first (cheapest grep), then single-quote, then the multi-flag awk fallback. - Extended `tr -d` in the awk fallback to strip both `"` and `'`, so the unquoted-token output is clean regardless of which quote style the original command used. Verification: - shellcheck → 0 issues - "grep -rn 'Symbol' src/" → "Symbol" (was: 'Symbol') - "grep -rn \"Symbol\" src/" → "Symbol" (regression) - "grep -i -r 'Symbol' src/" → "Symbol" (new, multi-flag + single quote) - "grep -i -r \"Symbol\" src/" → "Symbol" (regression) - "ls src/foo.tsx" → src/foo.tsx (regression) - "echo hello" → silent (regression) Created by Claude Code on behalf of @lapc506.
|
@dojo-code-reviewer review Round-3 feedback (4/5, 1 P3) addressed:
Smoke tests + shellcheck green. Fix commit: $(git rev-parse --short HEAD). |
There was a problem hiding this comment.
✅ Approved
Approved — 0 blockers, 1 P2, 3 P3. Confidence: 3/5.
🟡 P2 — Major
hooks/pre-tool-use-claim-verification.sh:81— 🟡 P2 (major) — The fallback regex"[^\"]*"stops at the first backslash (which JSON uses to escape internal double quotes), resulting in truncated command extraction when the executed bash command contains quotes (e.g.grep -rn \"Symbol\" src/). This causes subsequent command matching and warning suggestions to fail.
Using an escape-aware JSON string regex resolves this.
| grep -oE '"command"[[:space:]]*:[[:space:]]*"([^"\\]|\\.)*"' \
🔵 P3 — Minor
hooks/pre-tool-use-claim-verification.sh:164— 🔵 P3 (minor) — Redundant backslashes inside a single-quoted string are treated as literal backslashes by some shells and grep implementations, which can cause pattern extraction to fail or behave inconsistently depending on the platform's grep engine (e.g. macOS BSD grep vs GNU grep). Removing the backslashes before the double quotes inside single quotes improves cross-platform reliability.
Please also apply the same cleanup to the double quote backslashes on line 166.
| grep -oE 'grep[[:space:]]+-[rRnliN]+[[:space:]]+"[^"]+"' \
-
commands/verify.md:112— 🔵 P3 (minor) — Hardcoding-- src/restricts pattern verification to thesrc/directory. If the toolkit is installed in a repository that uses a different directory structure (e.g.,lib/,app/,packages/, or root-level files),git grepwill fail with a fatal pathspec error. Removing the-- src/suffix or falling back to the repo root ifsrc/is absent makes the command truly generic and repo-agnostic. -
commands/verify.md:45— 🔵 P3 (minor) — Remote-only tags will fail to resolve. If a tag (e.g.,v1.31.0) exists on the remote but has not been fetched locally yet,git rev-parsewill fail, pushing the ref to theelseblock which resolves it to the nonexistent branchorigin/v1.31.0. Pre-fetching tags or queryinggit ls-remotefirst would prevent this failure mode.
Total findings: 1 compliance, 3 business context (4 total)
ℹ️ Inline anchoring failed for the comments below; they are listed here instead.
hooks/pre-tool-use-claim-verification.sh:81— 🟡 P2 (major) — The fallback regex"[^\"]*"stops at the first backslash (which JSON uses to escape internal double quotes), resulting in truncated command extraction when the executed bash command contains quotes (e.g.grep -rn \"Symbol\" src/). This causes subsequent command matching and warning suggestions to fail.
Using an escape-aware JSON string regex resolves this.
| grep -oE '"command"[[:space:]]*:[[:space:]]*"([^"\\]|\\.)*"' \
hooks/pre-tool-use-claim-verification.sh:164— 🔵 P3 (minor) — Redundant backslashes inside a single-quoted string are treated as literal backslashes by some shells and grep implementations, which can cause pattern extraction to fail or behave inconsistently depending on the platform's grep engine (e.g. macOS BSD grep vs GNU grep). Removing the backslashes before the double quotes inside single quotes improves cross-platform reliability.
Please also apply the same cleanup to the double quote backslashes on line 166.
| grep -oE 'grep[[:space:]]+-[rRnliN]+[[:space:]]+"[^"]+"' \
commands/verify.md:112— 🔵 P3 (minor) — Hardcoding-- src/restricts pattern verification to thesrc/directory. If the toolkit is installed in a repository that uses a different directory structure (e.g.,lib/,app/,packages/, or root-level files),git grepwill fail with a fatal pathspec error. Removing the-- src/suffix or falling back to the repo root ifsrc/is absent makes the command truly generic and repo-agnostic.commands/verify.md:45— 🔵 P3 (minor) — Remote-only tags will fail to resolve. If a tag (e.g.,v1.31.0) exists on the remote but has not been fetched locally yet,git rev-parsewill fail, pushing the ref to theelseblock which resolves it to the nonexistent branchorigin/v1.31.0. Pre-fetching tags or queryinggit ls-remotefirst would prevent this failure mode.
…y (DOJ-4872)
Round-4 bot review at 3/5 confidence flagged 1 P2 + 3 P3. All four addressed.
P2 — hooks/pre-tool-use-claim-verification.sh JSON command extractor
- The jq-less fallback regex `"[^"]*"` stops at the first inner quote.
For a bash command like `grep -rn \"Symbol\" src/`, the JSON envelope
embeds it as `"command": "grep -rn \\\"Symbol\\\" src/"`, and the
naive regex truncates at the first `\"`. Subsequent detector / suggester
logic then sees a partial command and emits the wrong replacement.
- Switched to the escape-aware JSON-string body
`([^"\\]|\\.)*` which accepts any non-quote-non-backslash byte OR a
backslash followed by any byte. Verified with an envelope containing
`grep -rn \"Symbol\" src/` — the full command extracts cleanly and
the suggester emits the right line.
P3 — hooks/pre-tool-use-claim-verification.sh cross-platform quote handling
- The bot flagged that the explicit single-quoted-pattern grep extractor
(added in round 3) used embedded-quote shell escaping that can drift
between GNU and BSD grep. Simplified by removing the explicit single-
quoted branch entirely: the awk fallback already strips both `"` and
`'` from the extracted token, so it covers both quote styles without
embedded escapes. Net code reduction; same behavior across the four
smoke-tested variants (single/double quoted, single/multi flag).
P3 — commands/verify.md repo-agnostic grep path
- The behavior section hardcoded `-- src/` as the path filter, which
would fail with a `fatal: pathspec` error in toolkit consumers that
have a different layout (`lib/`, `app/`, `packages/`, flat). Added a
detect-or-fallback step: if `src/` exists on the resolved ref, use
it as the path filter; otherwise pass no path filter and grep the
whole tree.
P3 — commands/verify.md remote-only tag resolution
- The normalization step probed `git rev-parse --verify refs/tags/<tag>`
which only sees LOCAL tags. A remote-only tag (e.g. `v1.31.0` just
pushed by a teammate but not yet fetched locally) would fall through
to the bare-branch branch and resolve to a nonexistent
`origin/v1.31.0`. Added a `git ls-remote --tags --exit-code origin
refs/tags/<tag>` probe between the local-tag and SHA branches; on hit,
runs `git fetch origin tag <tag> --quiet` before resolving. Comment
documents the failure mode.
Verification:
- shellcheck → 0 issues
- JSON envelope with escaped quotes:
`{"tool_input":{"command":"grep -rn \\\"Symbol\\\" src/"}}` →
Command: `grep -rn "Symbol" src/` (was: `grep -rn `)
Suggested: `git grep "Symbol" origin/<branch> -- src/` (was: garbled)
- "grep -rn 'Symbol' src/" → Suggested: "Symbol" (regression)
- "grep -rn \"Symbol\" src/" → Suggested: "Symbol" (regression)
- "grep -i -r \"Symbol\" src/" → Suggested: "Symbol" (regression)
- "ls src/foo.tsx" → Suggested: src/foo.tsx (regression)
- "echo hello" → silent (regression)
Created by Claude Code on behalf of @lapc506.
|
@dojo-code-reviewer review Round-4 feedback (3/5, 1 P2 + 3 P3) addressed:
Smoke tests + shellcheck green. Fix commit: $(git rev-parse --short HEAD). |
There was a problem hiding this comment.
✅ Approved
Approved — 0 blockers, 4 P2. Confidence: 2/5.
🟡 P2 — Major
commands/verify.md:44— 🟡 P2 (major) — TheARGvariable is used in later steps (e.g.,"$ARG") but is never defined or assigned. This will cause$ARGto resolve to an empty string, breaking file path and pattern verification when Claude follows these instructions. AssigningARG="$2"next toRAW_REFensures proper parameter mapping.
RAW_REF="$1"
ARG="$2"
commands/verify.md:54— 🟡 P2 (major) — Runninggit fetch origin "tag $RAW_REF"(with quotes grouping thetagkeyword and the tag name together) passes them as a single argument to Git. This will cause the fetch operation to fail with a "does not appear to be a git repository" error. Separating them as distinct arguments ensures the remote-only tag is fetched successfully.
git fetch origin tag "$RAW_REF" --quiet
hooks/pre-tool-use-claim-verification.sh:99— 🟡 P2 (major) — The unanchored patterngrep[[:space:]]+-[rRnliN]+matches any occurrence of grep followed by flags, which includes legitimate Git commands likegit grep -n. This causes the hook to generate false-positive warnings even when users are running the exact ref-explicitgit grepcommands recommended by this very skill. Anchoringgrepto the start of the command or a separator prevents these false positives.
if printf '%s' "$cmd" | grep -qE '(^|[;&|][[:space:]]*)grep[[:space:]]+-[rRnliN]+'; then
hooks/pre-tool-use-claim-verification.sh:145— 🟡 P2 (major) — The unanchored patterngrep[[:space:]]+-[rRnliN]+matchesgit grephere as well, causing false positives in the suggestion logic. Anchoringgrepto the start of the command or a separator keeps the matching logic consistent with the detector.
elif printf '%s' "$COMMAND" | grep -qE '(^|[;&|][[:space:]]*)grep[[:space:]]+-[rRnliN]+'; then
Total findings: 4 compliance (4 total)
ℹ️ Inline anchoring failed for the comments below; they are listed here instead.
commands/verify.md:44— 🟡 P2 (major) — TheARGvariable is used in later steps (e.g.,"$ARG") but is never defined or assigned. This will cause$ARGto resolve to an empty string, breaking file path and pattern verification when Claude follows these instructions. AssigningARG="$2"next toRAW_REFensures proper parameter mapping.
RAW_REF="$1"
ARG="$2"
commands/verify.md:54— 🟡 P2 (major) — Runninggit fetch origin "tag $RAW_REF"(with quotes grouping thetagkeyword and the tag name together) passes them as a single argument to Git. This will cause the fetch operation to fail with a "does not appear to be a git repository" error. Separating them as distinct arguments ensures the remote-only tag is fetched successfully.
git fetch origin tag "$RAW_REF" --quiet
hooks/pre-tool-use-claim-verification.sh:99— 🟡 P2 (major) — The unanchored patterngrep[[:space:]]+-[rRnliN]+matches any occurrence of grep followed by flags, which includes legitimate Git commands likegit grep -n. This causes the hook to generate false-positive warnings even when users are running the exact ref-explicitgit grepcommands recommended by this very skill. Anchoringgrepto the start of the command or a separator prevents these false positives.
if printf '%s' "$cmd" | grep -qE '(^|[;&|][[:space:]]*)grep[[:space:]]+-[rRnliN]+'; then
hooks/pre-tool-use-claim-verification.sh:145— 🟡 P2 (major) — The unanchored patterngrep[[:space:]]+-[rRnliN]+matchesgit grephere as well, causing false positives in the suggestion logic. Anchoringgrepto the start of the command or a separator keeps the matching logic consistent with the detector.
elif printf '%s' "$COMMAND" | grep -qE '(^|[;&|][[:space:]]*)grep[[:space:]]+-[rRnliN]+'; then
…OJ-4872)
Round-5 bot review at 2/5 confidence flagged 4 P2. All four are real
correctness bugs (no nits this round); applied verbatim.
P2 — hooks/pre-tool-use-claim-verification.sh detector grep false-positive
- The unanchored pattern `grep[[:space:]]+-[rRnliN]+` matched any
grep-followed-by-flags substring, including the EXACT ref-explicit
replacement this hook recommends: `git grep -n "Symbol" origin/develop
-- src/`. Result: the hook warned against its own suggested fix,
creating a self-reinforcing false-positive loop. Anchored with
`(^|[;&|][[:space:]]*)` so `grep` must be at the start of the command
or after a separator, never inside `git grep`.
P2 — hooks/pre-tool-use-claim-verification.sh suggester grep false-positive
- Same unanchored pattern in the suggester branch caused the
`git grep` suggester to fire on legitimate `git grep` invocations
that fell through to the suggester (they shouldn't get there at all
if the detector is right, but defense-in-depth). Applied the same
`(^|[;&|][[:space:]]*)` anchor.
P2 — commands/verify.md ARG undefined
- The behavior section referenced `$ARG` in step 5 but never assigned
it. Following the example verbatim would leave `$ARG` empty and break
every verification. Added `ARG="$2"` next to `RAW_REF="$1"` at the
top of step 1.
P2 — commands/verify.md tag-fetch arg gluing
- `git fetch origin "tag $RAW_REF" --quiet` quotes the `tag` keyword
together with the tag name as a single arg, which git parses as a
refspec name → "does not appear to be a git repository" or "fatal:
couldn't find remote ref" depending on git version. Switched to
separate `tag` + `"$RAW_REF"` args, and refactored step 3 to use a
case-statement dispatcher over `$FETCH_KIND` ("tag" | "branch" | "")
so the tag-vs-branch fetch shapes stay distinct.
Verification:
- shellcheck → 0 issues
- "grep -rn \"Symbol\" src/" → warn (regression)
- "git grep -n \"Symbol\" origin/develop -- src/" → silent (was: WARN — the hook
was attacking its
own recommended fix)
- "cd src && grep -rn \"Symbol\" ." → warn (chain-after-separator works)
- "echo hello" → silent (regression)
Created by Claude Code on behalf of @lapc506.
|
@dojo-code-reviewer review Round-5 feedback (2/5, 4 P2) addressed — all four were real correctness bugs:
Smoke tests + shellcheck green. Fix commit: $(git rev-parse --short HEAD). |
main advanced 6 commits (1.31.0 -> 1.35.0) while this branch sat open, so every shared manifest conflicted. Resolution keeps main on everything except this branch's own contribution. Taken from main: - package.json / plugin.json / marketplace.json version -> 1.35.0 - marketplace + package descriptions (main's escaped form) - CHANGELOG entries 1.35.0 / 1.34.0 / 1.33.0 / 1.32.0 and the link footer - README version line and the /observability-audit + /parallelize table rows Kept from this branch: - commands/verify.md, skills/verify-branch-state/SKILL.md, hooks/pre-tool-use-claim-verification.sh (merged cleanly into hooks.json) - the /verify README row and the verify-branch-state skill row - the marketplace "Pair it with verify-branch-state" sentence - the 1.31.0 CHANGELOG entry, placed in chronological order after 1.32.0 Counts recomputed from the merged tree rather than carried over: 38 commands (was 37 on main, 30 on the branch) and 12 skills (was 11 on both). Verified on the merge result: vitest 60/60, hooks/test-hooks.sh 328/328, hooks/test-sentinel-walkup.sh 5/5, and `npm run build-rules` leaves hooks/rules/rules.json unchanged. NOTE for the PO: the 1.31.0 CHANGELOG entry describes a version that was never released (main went 1.30.0 -> 1.32.0). Whether this ships as a back-filled 1.31.0 or gets renumbered is a release decision, left untouched.
There was a problem hiding this comment.
✅ Approved
Approved — no findings. Confidence: 5.00/5.00.
Walkthrough
main branch directly. Standard GitFlow workflow expects features to target develop first (feature → develop → main). Since the code itself is functionally sound and fully backward-compatible, this is an informative warning and does not block approval.
Walkthrough
This PR implements the verify-branch-state skill, the /verify slash command, and the plugin-wide pre-tool-use-claim-verification.sh hook. It enforces the core discipline that the local working tree is only one projection of the repository, while the remote/fetched ref is the absolute truth, preventing agent footguns when answering branch-level questions in multi-worktree/multi-session environments.
Files Reviewed
hooks/pre-tool-use-claim-verification.sh(Warn-mode detector, awk suggestions, fail-open logic)commands/verify.md(Ref-explicit command, tag/branch/SHA normalization, remote tag fetching)skills/verify-branch-state/SKILL.md(Auto-triggering skill definition, checklist, incident reference)hooks/hooks.json(Registration under theBashmatcher with 3s timeout).claude-plugin/marketplace.json,CHANGELOG.md, andREADME.md(Release manifest updates and documentation additions)
Safety Rationale
The hook operates entirely in warn-mode (set +e and guaranteed exit 0 on all paths) and fails open gracefully if jq or git tools are missing or if any path resolution fails, making it extremely safe to deploy.
Verdict
Approved — no findings.
Measured on origin/main @ ee0ba47: the three version files all read 1.36.0, and `gh pr diff 55 | grep -E '^\+.*"version"'` shows #55 already bumps to 1.37.0 in package.json, plugin.json and marketplace.json. Taking 1.37.0 here collides with it in all three. The same probe over #52, #46, #44 and #41 returns nothing, so 1.38.0 is free. Skipping a version costs nothing; colliding does. Minor and not major, decided by reading. A skill auto-activates on its `description` rather than being invoked by name like a command, so a renamed `name:` changes no call site. The reference search returns nothing: grep -rniI "rebase.advisor" . --exclude-dir=node_modules \ --exclude-dir=.git --exclude=CHANGELOG.md -> no hits That negative is real and not a broken search -- the same grep for `spike-recommend` returns 10 files, so cross-references of this shape are found when they exist. One surface does break and is stated rather than folded in: a user who typed `/make-no-mistakes:rebase-advisor` explicitly (README:137 documents that skills can be invoked that way) now gets an unknown skill. It fails loudly, the replacement is one row away in the same table, and the installer prunes the old file rather than leaving both live. The `[1.37.0]:` CHANGELOG reference slot is deliberately left for #55 to fill. Suite: 337/337 hooks, 60/60 vitest. Created by Claude Code on behalf of @lapc506
…(v1.38.0) (#56) * feat(skills): sync-advisor — measure the drift before naming the fix (v1.37.0) Renames `rebase-advisor` to `sync-advisor` and turns a blind router into an advisor that measures. The old skill was 43 lines and measured nothing. Its step 1 was "Confirm the user wants a full team sync" -- a question back to the user about something three git commands answer. Its description then over-routed: it triggered on "align with develop" and "branches are behind", both of which are `git pull`, and sent them to /make-no-mistakes:rebase, which stashes every worktree, rebases every local branch and auto-merges PRs. Between `git pull` and that the toolkit offered nothing, and nothing read-only at all. Six read-only predicates now run before anything is named: distance, fast-forward possible, dirty tree split by stage, untracked files the ref already tracks, worktrees behind, and branches with unpushed commits. The fifth is the threshold between a plain pull and the team command. The fourth is the one nothing else reports, and it was verified on a throwaway pair of repos rather than asserted. An untracked local file at a path the ref tracks aborts the pull outright while being invisible everywhere else: `git status` shows a plain `?? newfile.txt`, distance reports a clean 0 ahead 1 behind, and `merge-base --is-ancestor` says a fast-forward is possible. The pull then exits 1 with "The following untracked working tree files would be overwritten by merge ... Please move or remove them before you merge" -- the message names the user's own file and offers deletion as the remedy, which is the one irreversible move available. The skill reports these by name and recommends copying them out of the repo, never deleting them. Both controls were run: the pipeline printed nothing before the collision existed and named the file after. It never acts. Every fix is printed for the user to run. The single write is `git fetch origin --quiet`, which touches remote-tracking refs and nothing else, and the skill says so out loud -- without it every measurement is taken against a stale origin/<base> and reports a drift that stopped being true days ago, which is the failure this skill exists to catch. Adds `syncAdvisor.governedPaths` to make-no-mistakes.config.json: the paths whose changes get reported by name, turning "you are 12 behind" into "three hooks changed, two of them fix defects you may be looking at right now". No default -- with the key unset the skill drops the consequence line rather than falling back to a built-in list, which would be wrong in every repo but the one it was copied from and would read as measured. commands/rebase.md is untouched and stays a real destination. What changed is who decides when it applies. Origin (2026-07-31, as reported): a developer filed two bug reports against a hook with clean reproductions. One was a real defect; the other described behaviour fixed days earlier against a stale checkout, and nothing in the report separated them. Suite: 337/337 hooks, 60/60 vitest. Created by Claude Code on behalf of @lapc506 * chore(release): take 1.38.0 instead of 1.37.0 — #55 owns 1.37.0 Measured on origin/main @ ee0ba47: the three version files all read 1.36.0, and `gh pr diff 55 | grep -E '^\+.*"version"'` shows #55 already bumps to 1.37.0 in package.json, plugin.json and marketplace.json. Taking 1.37.0 here collides with it in all three. The same probe over #52, #46, #44 and #41 returns nothing, so 1.38.0 is free. Skipping a version costs nothing; colliding does. Minor and not major, decided by reading. A skill auto-activates on its `description` rather than being invoked by name like a command, so a renamed `name:` changes no call site. The reference search returns nothing: grep -rniI "rebase.advisor" . --exclude-dir=node_modules \ --exclude-dir=.git --exclude=CHANGELOG.md -> no hits That negative is real and not a broken search -- the same grep for `spike-recommend` returns 10 files, so cross-references of this shape are found when they exist. One surface does break and is stated rather than folded in: a user who typed `/make-no-mistakes:rebase-advisor` explicitly (README:137 documents that skills can be invoked that way) now gets an unknown skill. It fails loudly, the replacement is one row away in the same table, and the installer prunes the old file rather than leaving both live. The `[1.37.0]:` CHANGELOG reference slot is deliberately left for #55 to fill. Suite: 337/337 hooks, 60/60 vitest. Created by Claude Code on behalf of @lapc506 * fix(sync-advisor): make the collision predicate correct from any subdirectory Reviewer P3 on `ea8c46e` claimed `git ls-tree` "always outputs repository-relative paths" while `git ls-files` is prefix-relative, so the two would fail to match from a subdirectory, and proposed `--full-name` on `ls-files`. Measured: the premise is false and the proposed fix introduces the bug it claims to prevent. From `sub/`, `ls-tree -r --name-only` prints `newfile.txt`, not `sub/newfile.txt` -- it strips the prefix exactly like `ls-files` does, so the original command matched fine and found the collision. Adding `--full-name` alone then makes `ls-files` emit `sub/newfile.txt` against `ls-tree`'s `newfile.txt`, they stop matching, and `comm -12` returns empty -- a clean bill of health for a tree about to abort the pull. But it pointed at a real weakness of a different kind. From a subdirectory both commands are SCOPED to that subtree, so a collision at the repo root is not seen at all. That is scope, not format, and no combination of format flags fixes it: `--full-name` changes how a path prints, never which paths are considered. The pathspec does. Shipped: `--full-name -- :/` on ls-files, `--full-tree` on ls-tree. Four cases run, with the command extracted verbatim from SKILL.md so the test cannot drift from the doc -- negative control from the root and from `sub/` (both empty), positive from the root and from `sub/` with one collision in each location (both list `rootfile.txt` and `sub/newfile.txt`). Suite: 340/340 hooks, 60/60 vitest. Created by Claude Code on behalf of @lapc506 * fix(sync-advisor): resolve the base ref correctly, and in a bare form Reviewer P2 on `00e2c08`, and it is right. Step 0 returned values carrying the remote prefix while every predicate interpolates `origin/$BASE`, so the run died on `origin/origin/develop`: $ git rev-list --left-right --count "HEAD...origin/origin/main" fatal: ambiguous argument … unknown revision exit=128 Fixed by normalising unconditionally (`${BASE#refs/remotes/}`, `${BASE#origin/}`) whichever branch of the resolution produced the value. Verified both branches with a control that must fail: where `origin/HEAD` IS set it returns `origin/main`, normalises to `main`, and `origin/main` resolves; the un-normalised form exits 128. The P2 understated it. Step 0's first command resolved the base from `@{upstream}`, which on a feature branch is that branch's OWN remote copy -- here `origin/andres/sync-advisor`. That answers "am I pushed?", which is predicate 6's question, and would report 0 behind on a branch far behind the real base. Stripping a prefix would have left it pointing at the wrong ref, so the command is gone from base resolution rather than patched. Also measured while there: `git symbolic-ref --short refs/remotes/origin/HEAD` fails outright in this repo (`not a symbolic ref`), so it is documented as a fall-through into the develop/main/master/trunk probe rather than as a step that is expected to succeed. P4 (`--abbrev-ref` and `--symbolic-full-name` redundant) is also correct -- both forms return `origin/andres/sync-advisor` here -- and is moot: that command no longer appears. Suite: 340/340 hooks, 60/60 vitest. Created by Claude Code on behalf of @lapc506
Summary
Plugin-layer (Level 1) extension of the working-tree-vs-ref discipline rule. This release operationalizes memory
feedback_working_tree_is_not_truthat the toolkit layer so the rule travels with the plugin to every repo it's installed in.Incident origin (2026-06-03). A re-audit subagent correctly flagged
usePathwayViewModel.tsas deleted ondevelopduring DOJ-4849 triage. The orchestrator "corrected" the subagent by runningls src/hooks/path/usePathwayViewModel.tsagainst a local main checkout that another session had switched to a different branch. The bare-FS read returned a stale projection; authoritative "FRESH — proceed as briefed" comments went to DOJ-4851/4863/4864 and had to be retracted publicly. Root cause: working tree was treated as truth instead of one projection.Defense in depth. Earlier today, DOJ-4868 shipped the repo-layer (Level 2) enforcement via
.claude/hooks/+ rootCLAUDE.mdindojo-os(PR #2604). DOJ-4872 ships the plugin-layer (Level 1) here: skill + slash command + warn-mode PreToolUse hook. Without it, agents usingmake-no-mistakes-toolkitin any other repo have zero guidance about the rule.Linear: DOJ-4872
Sibling: DOJ-4868 (dojo-os, Level 2)
Memory:
feedback_working_tree_is_not_truthRelease: v1.30.0 → v1.31.0
What ships
4 new artifacts:
skills/verify-branch-state/SKILL.md— auto-loading skill, tuned description triggers on "verify against develop", "does X exist on<branch>", "what's on origin/…", "did this land on main", "re-validate the subagent", "before I claim", "post-merge state". Body covers the rule statement, canonical ref-explicit commands (git fetch + git show / ls-tree / grep / diffagainstorigin/<ref>), anti-patterns (ls,grep -rn src/,find,git status), pre-flight checklist, multi-worktree context, and the 2026-06-03 origin incident.commands/verify.md— slash command/verify <ref> <path-or-pattern>. Runsgit fetch origin <ref> --quietthengit ls-tree origin/<ref> -- <path>for paths orgit grep <pattern> origin/<ref> -- src/for patterns. Emits a single verdict line citing the ref's short SHA in the shapeorigin/<ref> @ <sha7> — <path-or-pattern> <PRESENT|NOT PRESENT|N MATCHES>so the claim can be re-checked later. Handles ref-not-found, shell metacharacters in patterns, and squash-merge cache staleness (always re-fetches).hooks/pre-tool-use-claim-verification.sh— warn-mode PreToolUse hook, repo-agnostic, no per-repo manifest required. Detects bare-FS verification ops (ls,grep -rn src/,find src/,cat src/,head src/) and, when combined with intent keywords in the recent context envelope (orCLAUDE_HOOK_CONTEXTfor smoke tests), prints a stderr warning with the local checkout's HEAD ref + short SHA, a suggested ref-explicit replacement specific to the detected op, and pointers to the skill + slash command. Alwaysexit 0. Defensiveset +e. Usescd -P+pwd -Pfrom day 1 (DOJ-4868 PR #2604 lesson). Trailing-newline-safewhile read. HonorsCLAUDE_DISABLE_PLUGIN_HOOKS=1. Shellcheck clean.CHANGELOG.md[1.31.0] - 2026-06-03— Added / Changed / Why sections + reference link added at bottom.4 edits:
hooks/hooks.json— registers the new hook under theBashPreToolUse matcher with a 3-second timeout, alongside the existingpre-bash.shdispatcher..claude-plugin/marketplace.json— top-level + pluginversion1.30.0 → 1.31.0; plugin description pair-mentions the new skill; component counts updated to 30 commands / 11 skills / 2 agents..claude-plugin/plugin.json—version1.30.0 → 1.31.0.package.json—version1.30.0 → 1.31.0.README.md—**Version:**header bumped; new "Also start here:verify-branch-state+/verify" peer subsection sits between thedomain-driven-advisor"Start here" block and "What's Inside"; Commands table 29 → 30 (adds/verify); Skills table 10 → 11 (addsverify-branch-state).Cohesive-PR exception (9 files)
9 files instead of the soft ≤8 target. The 4 new artifacts + 4 release-cut edits sum to 8; the 9th is
hooks/hooks.json, which is required to make the new hook non-dead code. The whole set is a single cohesive surface (working-tree-vs-ref discipline) plus the standard release-cut shape mirroring v1.30.0's commit shape. Documented per CLAUDE.md rule 7 cohesive-PR exception.Per-AC ship status
/verify <ref> <path-or-pattern>cites ref SHAset +e,cd -P+pwd -P,exit 0always, shellcheck cleanjq[1.31.0] - 2026-06-03+ reference linkfeedback_working_tree_is_not_truthv1.31.0Smoke test outputs (AC7)
Post-merge actions (deferred to maintainer)
These are explicitly out of this PR's scope and will be performed after merge by @lapc506:
git tag -a v1.31.0 -m "v1.31.0"+git push origin v1.31.0(AC10)#doj-repo-health(Slack thread)Test plan
jq -r '.version' …× 4)ls, silent onecho, warn ongrep -rn, warn onfindshellcheck hooks/pre-tool-use-claim-verification.sh→ 0 issuesCLAUDE_DISABLE_PLUGIN_HOOKS=1(earlyexit 0)cd -P+pwd -Pused for hook self-location (day 1, per DOJ-4868 PR #2604)descriptionfield tuned for auto-trigger on the canonical intent phrases[1.31.0]: …/releases/tag/v1.31.0)domain-driven-advisorstyleCreated by Claude Code on behalf of @lapc506.