task-reviewer: tracker-aware spec-issue cross-check (claude-jira/notion/local) (#144)#145
Conversation
…on/local) (#144) The dispatcher and the `/reviewer` prompt were both GitHub-only: numeric / URL parsing in `parse_issue_number`, `Closes/Fixes/Resolves #N` body auto-detect, and the GH-flavored URL synthesis — plus the prompt hardcoded `gh issue view <M>` for spec lookup. Net result: `task-reviewer <pr> <spec-id>` in claude-jira / claude-notion / claude-local silently degraded to a diff-only review. Dispatcher: keep the 4 bodies byte-identical (`task-reviewer-claude` drift group stays intact); detect the loadout from the dispatcher's own file path and branch internally. claude-gh keeps numeric / GH-issue-URL parsing and PR-body auto-detect; non-gh loadouts accept any non-empty 2nd positional arg as an opaque spec identifier and pass it through to /reviewer unchanged. With no 2nd arg, non-gh emits a clear "PR-body auto-detect is GitHub-only — pass the spec id explicitly" advisory and proceeds diff-only. Prompts: legitimately differ per loadout (the previous byte-identical state was accidental — never part of the drift manifest). claude-jira points spec lookup at `mcp__atlassian__getJiraIssue`; claude-notion at `mcp__notion__notion-fetch`; claude-local at `Read("tasks/<id>-<slug>.md")`. Shared spine (authority boundaries, radio handoff intents, verdict structure) stays consistent — pinned by two new assertions in `tests/drift_check.bats` that replace the old byte-identical check. Tests: extend `tests/task_reviewer.bats` with per-loadout pass-through coverage for Jira keys / Notion URLs / local slugs; ignore-GH-Closes-in-PR- body diff-only behavior; and the new advisory copy. claude-gh path stays green (regression test pins the original GitHub-flavored advisory). Docs: README's `task-reviewer` section now lists each loadout's spec-id shape and notes that PR-body auto-detect is GH-only. CHANGELOG `Unreleased` gets a `Fixed` entry referencing #144 (lands before #143's v0.3.0 cut). Closes #144 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec compliance (vs #144)PR delivers all four parts of the spec:
Manual smoke tests (claude-jira scratch repo + claude-gh regression) are explicitly marked unchecked in the PR — acceptable given they require a live Jira environment. Code-review findings1.
|
Changes requested — three pre-merge fixes; two findings filed as follow-upsReviewer's 5 findings triaged. Three to fix before merge; two to track separately. Pre-merge1.
Cheapest fix: Add a bats test that passes a 3. Other tools in the same prompt are explicit ( 4. The claude-jira and claude-local advisory tests both assert Follow-ups (filed separately, NOT for this PR)
Both are P2 and not v0.3.0 blockers. Don't touch them in this PR. Out of scope for this roundResist the urge to add other refactors. Push the three fixes above, run the suite, and radio |
…b, notion advisory assertion (#144) Three pre-merge fixes from PM review: 1. **`set +H` in build_claude_cmd**: prepend `set +H;` to the assembled `claude /reviewer ...` command so the spawned `bash -ic` subshell has history expansion disabled. SPEC_IDENTIFIER values containing `!` (Jira key with v2 suffix, local slug like `auth-login!v2`, etc.) would otherwise risk being silently history-substituted before /reviewer ever saw the arg. The widening to opaque non-gh identifiers in this PR is what made the existing surface a real concern. Synced across the 4 dispatcher bodies (drift sentinel preserved). Two new bats tests pin the defense: `!`-in-SPEC_IDENTIFIER lands unmangled in the recorded command, and the assembled command starts with `set +H;` on both non-gh and gh paths. 2. **claude-local prompt: explicit `Glob` tool**: replace the bare verb "glob `tasks/<arg>*.md`" with an explicit `Glob("tasks/<arg>*.md")` tool call, matching the style of every other tool invocation in the prompt (`Read(...)`, `gh pr view`, `mcp__atlassian__getJiraIssue`). Adds a "do not fall back to Bash(find)" guardrail so the reviewer doesn't trip a permission prompt under `--permission-mode auto` on a path the harness wouldn't pre-allow. 3. **claude-notion advisory test: missing assertion**: add the `--partial "diff-only review"` assertion that claude-jira and claude-local already have. A future regression that dropped the trailer for notion-only would otherwise be invisible to CI. Out of scope for this PR (filed as follow-ups, per PM): kiro-gh migration to SPEC_IDENTIFIER + AW_LOADOUT (#146); .claude/gh-workflow.md doc drift (#147). Full suite: 755/755 green. Drift check: 21 groups passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-review: PR #145 (round 2, addressing PM's changes-requested)Pre-merge fixes from round-1: statusAll three items from PM's changes-requested are addressed in this push:
New tests for Blocker found in round-2 changes
Before this PR the assembled command was:
After this PR The prefix vars are now assignments to $ bash -c 'MYVAR=hello set +H; printf "[%s]\n" "$MYVAR"'
[] # ← empty: MYVAR never reached printfSo when The tests don't catch this because Fix — move - aw_launch_tab "$SLUG" "$WORKTREE_DIR" "${RADIO_ENV_PREFIX}$(build_claude_cmd)" "1"
+ aw_launch_tab "$SLUG" "$WORKTREE_DIR" "set +H; ${RADIO_ENV_PREFIX}$(build_claude_cmd)" "1"And - echo "set +H; claude ${mode_prefix}\"/reviewer $slash_args\""
+ echo "claude ${mode_prefix}\"/reviewer $slash_args\""With this ordering: Add a bats assertion that the assembled command has Nit (non-blocking)
Verdict:
|
Changes requested — round 3Blocker (regression introduced in round-2)
Bash scopes Result: Fix — move - aw_launch_tab "$SLUG" "$WORKTREE_DIR" "${RADIO_ENV_PREFIX}$(build_claude_cmd)" "1"
+ aw_launch_tab "$SLUG" "$WORKTREE_DIR" "set +H; ${RADIO_ENV_PREFIX}$(build_claude_cmd)" "1"- echo "set +H; claude ${mode_prefix}\"/reviewer $slash_args\""
+ echo "claude ${mode_prefix}\"/reviewer $slash_args\""That ordering: Apply the same fix in all 4 claude- Add a bats test that confirms the ordering: Nit (small, take while you're in there)
assert_output --partial "diff-only review"Out of scope for round 3Don't touch anything else. Round 3 is: re-order Radio |
…144 round-3) Round-2 introduced `set +H;` inside `build_claude_cmd`'s output, between the RADIO_ENV_PREFIX assignments and `claude`. Bash scopes `VAR=val cmd1; cmd2` assignments to `cmd1` only — so the env prefix attached to the `set` builtin and `claude` never received `TASK_FORCE_ROLE`, `ZELLIJ_TAB`, `ANTHROPIC_MODEL`, or `TASK_FORCE_AUTO_SUBMIT`. Radio routing, Sonnet override, and --auto AUTO_SUBMIT all silently broke. The existing `assert_stub_called zellij` checks only see the command string handed to zellij — they don't evaluate what `bash -ic` actually runs, so the regression was invisible to CI. Fix: drop `set +H;` from `build_claude_cmd` and prepend it at the `aw_launch_tab` call site BEFORE `${RADIO_ENV_PREFIX}`: aw_launch_tab ... "set +H; ${RADIO_ENV_PREFIX}$(build_claude_cmd)" "1" Now the spawned shell sees: `set +H; TASK_FORCE_ROLE=... claude ...`. `set +H` runs (disabling histexpand for the subshell), then the env prefix correctly binds to `claude` (an external command). Synced across the 4 dispatcher bodies (drift sentinel still intact). New bats coverage: - Ordering assertion: `set +H;` must precede `TASK_FORCE_ROLE=` in the assembled command. This regression test would have caught round-2. Also fixed (round-3 nit): claude-local advisory test was missing `assert_output --partial "diff-only review"` — the assertion was added to claude-jira / claude-notion in earlier rounds but the claude-local sibling was missed. A regression that dropped the trailer for local would have been invisible to CI. Full suite: 756/756 green. Drift check: 21 groups passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-3 review: PR #145Pre-merge items from PM's round-3 changes-requested: statusAll three explicitly required items are addressed:
Round-2 pre-merge items (bare New finding from this round
PM's round-3 comment said: "Apply to both
aw_launch_tab "$SLUG" "$WORKTREE_DIR" "${RADIO_ENV_PREFIX}$(build_claude_cmd)" "${AUTO_MODE:-}"And its echo "claude ${mode_prefix}\"/worker Implement task: $GH_URL\""
Fix is a one-liner at Nit (non-blocking)Ordering test edge case — false pass when The ordering test uses prefix-substring comparison: local set_pos="${cmd_line%%set +H;*}"
local role_pos="${cmd_line%%TASK_FORCE_ROLE=*}"
[[ ${#set_pos} -lt ${#role_pos} ]] || ...If Verdict:
|
Changes requested — round 4Required:
|
…round-4)
PM's round-3 ask explicitly extended to task-work: "Apply to both
task-reviewer and (if the same idiom exists there) task-work." The idiom
exists at line ~229 across all 7 task-work dispatchers — same
aw_launch_tab call, same bash -ic exposure, same RADIO_ENV_PREFIX
env-prefix ordering concern. build_claude_cmd / build_kiro_cmd embeds
user-provided URLs ($GH_URL, Jira URL, Notion URL) directly in the
assembled command string; GitHub URLs don't normally contain `!`, but
the unprotected path is live.
Fix: prepend `set +H; ` at the aw_launch_tab call site, BEFORE
${RADIO_ENV_PREFIX}, in all 7 dispatchers (claude-gh, claude-jira,
claude-local, claude-notion, kiro-gh, kiro-local, kiro-notion). The 6
drift-grouped files (task-work-claude + task-work-kiro post-info regions)
keep their drift-group byte-equality intact; claude-jira's launch site
isn't drift-grouped so it carries its own copy. Drift check passes.
New bats coverage in tests/claude_gh_task_work.bats: ordering assertion
mirroring the task-reviewer test — pins `set +H;` before
TASK_FORCE_ROLE= in the assembled command. A regression that
re-introduces `set +H` inside build_claude_cmd's output would now fail
the test.
Also hardened (round-3 nit): the task_reviewer ordering test now
explicitly pins presence of both `set +H;` and `TASK_FORCE_ROLE=` before
the prefix-length comparison, so a regression that drops TASK_FORCE_ROLE=
from the assembled command no longer false-passes the ordering check.
Full suite: 757/757 green. Drift check: 21 groups passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-4 review: PR #145Round-4 pre-merge items: statusAll three items from PM's round-4 changes-requested are addressed:
New findings from round-41.
echo "claude ${mode_prefix}\"/reviewer $slash_args\""where
For canonical Jira keys ( The - [[ -n "$SPEC_IDENTIFIER" ]] && slash_args+=" $SPEC_IDENTIFIER"
+ [[ -n "$SPEC_IDENTIFIER" ]] && slash_args+=" $(printf %q "$SPEC_IDENTIFIER")"2. Dead code:
3.
Verdict:
|
Changes requested — round 5 (closes the shell-quoting class)Required:
|
…quoting class (#144 round-5) `set +H` (rounds 3-4) only blocked `!` history expansion. The child `bash -ic` shell still expanded `$var`, `$(...)`, backticks, and other metacharacters inside double-quoted strings — so SPEC_IDENTIFIER (in task-reviewer) and the per-loadout user-input vars in task-work (GH_URL / JIRA_REF / TASK_FILE / NOTION_URL) were still vulnerable. RADIO_ENV_PREFIX values already use `printf %q` for exactly this reason; the inconsistency was the bug. Fix: apply `printf %q` to every user-input string embedded into the assembled bash -ic command. - task-reviewer (drift-grouped — fixed once, propagated to 4 copies): SPEC_IDENTIFIER → `printf %q` before concatenation into slash_args. - task-work (NOT drift-grouped; per-loadout vars differ): - claude-gh: $GH_URL (used in --plan and worker paths) - claude-jira: $JIRA_REF - claude-local: $TASK_FILE - claude-notion: $NOTION_URL - kiro-gh: $GH_URL - kiro-local: $TASK_FILE - kiro-notion: $NOTION_URL Each gets a local q_<var> = $(printf %q "$<var>") shadow used in the echoed command. New bats coverage: - tests/task_reviewer.bats: SPEC_IDENTIFIER with `$HOME-test` and `PROJ-$(date)` lands escaped in the recorded zellij call (i.e. the child shell can't expand them). Existing `!`-test updated to expect the bash-3.2 `\!` escaped form. - tests/claude_gh_task_work.bats: same shape for GH_URL embedded in query strings (`?$HOME`, `?q=$(date)`). Also took the optional dead-code nit: dropped the unreachable `"spec identifier cannot be empty"` else-branch in task-reviewer's parse_issue_number failure path (the call site already guards on [[ -n "$ISSUE_INPUT" ]], so the empty-input return path is claude-gh-only). After this round, all user-input entering bash -ic is shell-escaped. The remaining strings in build_*_cmd are static literals or ${mode_prefix} (also literal). Shell-quoting class is closed. Drift check: 21 groups passing. Targeted suites green; task_reviewer + claude_gh_task_work both pass with new round-5 tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-5 review: PR #145Round-5 pre-merge items: statusAll three items from PM's round-5 changes-requested are correctly addressed:
The dead-code nit from round-4 ( New finding from round-5 analysis
printf 'ISSUE_NUMBER=%s\n' "$ISSUE_NUMBER"which produces an unquoted assignment line:
Before this PR, Practical risk is low — real Jira keys ( Straightforward fix (can be a follow-up): in task-done, replace Minor nit (informational)
When
So Verdict:
|
Summary
Fixes #144.
task-reviewer's spec-issue cross-check was GitHub-only — both the bash dispatcher (numeric/URL parsing,Closes #NPR-body auto-detect, GH URL synthesis) and the/reviewerslash-command prompt (hardcodedgh issue view <M>). In claude-jira / claude-notion / claude-local,task-reviewer <pr> <spec-id>silently degraded to a diff-only review.Dispatcher (Part A) — internal-dispatch: 4 dispatcher bodies stay byte-identical (
task-reviewer-claudedrift group untouched), detect the loadout from the dispatcher's own file path, and branch on$AW_LOADOUT.claude-ghkeeps numeric / GitHub-issue-URL parsing and PR-body auto-detect;claude-jira/claude-notion/claude-localaccept any non-empty 2nd positional arg as an opaque spec identifier (Jira key, Notion URL, local slug) and pass it through to/reviewerverbatim. With no 2nd arg, non-gh emits a "PR-body auto-detect is GitHub-only" advisory and proceeds diff-only.Prompts (Part B) —
commands/reviewer.mdnow legitimately differs per loadout (the previous byte-identical state was accidental — never part of the drift manifest).claude-jirausesmcp__atlassian__getJiraIssue;claude-notionusesmcp__notion__notion-fetch;claude-localreadstasks/<id>-<slug>.mdvia theReadtool. The shared spine (authority boundaries, radio handoff intents, verdict structure) stays consistent — pinned by two new assertions intests/drift_check.batsthat replaced the old byte-identical check.Tests (Part C) —
tests/task_reviewer.batsextended with per-loadout pass-through (Jira key / Notion URL / local slug), ignore-GH-Closes-in-PR-body diff-only behavior, and the new advisory copy.claude-ghpath stays green; a regression test pins the original GH-flavored advisory.Docs (Part D) — README's
task-reviewersection lists each loadout's spec-id shape and the non-gh constraint. CHANGELOG[Unreleased]gets aFixedentry referencing #144 (lands before #143's v0.3.0 cut).Test plan
./run_tests.sh task_reviewer→ 75/75 green (10 new task-reviewer: spec-issue cross-check is GitHub-only — breaks claude-jira / claude-notion / claude-local #144 cases)./run_tests.sh drift_check→ 8/8 green (replaces old byte-identical reviewer.md check)./run_tests.sh(full suite) → 753/753 greentools/check-drift.sh→ 21 groups checked, all pass (task-reviewer-claude drift sentinel still intact)task-reviewer <pr> PROJ-Nspawns a reviewer that reads the Jira issue via MCPUpgrading
Re-run
task-init <loadout>(claude-jira / claude-notion / claude-local) after pulling so the updated/reviewerprompt lands in.claude/commands/. Without the re-run, the spawned reviewer would still use the old GitHub-only prompt and trygh issue view PROJ-123.Closes #144
🤖 Generated with Claude Code