fix(hooks): anchor fence-marker detection to line start in has_duplicate_waiver - #1715
Conversation
…ate_waiver has_duplicate_waiver() returned False for a Duplicate-PR-waiver: line whose disclosed reason was itself fence-shaped (e.g. ~~~), because _FENCE_RE/_UNTERMINATED_FENCE_RE matched a fence marker anywhere in the text rather than only at the start of its own line -- swallowing the reason before _WAIVER_RE ever saw it. Confirmed reproducing identically on origin/main, independent of any PR, via direct function call. Ports the exact, already-adversarially-hardened _CONTAINER_PREFIX fix issue #1432/PR #1440 applied to the sibling copy in skills/drafting-issues/scripts/gitapex_check_acm_present.py (list-item/ blockquote-prefixed fences, bare-CR line endings, and a ReDoS-safe hoisted-whitespace construction) -- not re-derived, to avoid reintroducing either gap that fix's own two review rounds already closed. Six new regression tests mirror that fix's own five, adapted to this file's naming. Refs #1714 pytest: 8011 passed (was 8006; plus 5 new tests). 44/44 local-preflight gates passed.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1715 +/- ##
=======================================
Coverage 99.67% 99.67%
=======================================
Files 153 153
Lines 24963 24990 +27
Branches 3010 3010
=======================================
+ Hits 24881 24908 +27
Misses 82 82 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…cate_waiver An isolated evaluating-deterministic-gate-quality review (dispatched for drafting-a-pr-to-merge Step 8's inner layer on PR #1715) live-verified two gaps in this PR's own new tests: - test_waiver_line_rejected_inside_a_bare_cr_fenced_block's docstring claimed to pin _CONTAINER_PREFIX's own `\r` alternative, but has_duplicate_waiver() normalizes CR to LF before _strip_fences() ever runs, so that test only exercised the normalization step, not the regex branch it claimed to. Confirmed live: rebuilding _CONTAINER_PREFIX without the `\r` alternative and re-running the same payload through the same pipeline left the test passing unchanged. Corrected the docstring to describe what it actually pins, and added a new test calling _strip_fences() directly on unnormalized text to genuinely exercise the `\r` branch -- confirmed this new test fails without that branch present. - The container prefix's blockquote (`>`) alternative was live-verified correct but exercised by none of the five original new tests, a real gap against this PR's own defeat-test-disclosure claim. Added a matching blockquote-fenced regression test. Refs #1714 pytest: 50/50 passed (hooks/test_gitapex_check_pr_duplicate_issue.py + tests/test_gitapex_check_pr_duplicate_issue_properties.py). ruff/mypy clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7HpPtaimtUthJqyST8qx
…e_waiver A second isolated round-2 verification review (dispatched to confirm the prior round's two findings were actually closed) found that my own fix for one of them was itself defective: test_waiver_line_rejected_inside_a_blockquote_fenced_block asserted through has_duplicate_waiver() alone, but _WAIVER_RE's own line-prefix (^[ \t]*[-*]?[ \t]*) never permits a `>` marker at all -- so the negative assertion passed regardless of whether the blockquote-fenced block was ever actually stripped. Confirmed live: reconstructing _CONTAINER_PREFIX without its `>` alternative left the test passing unchanged. Fixed the same way the bare-CR test was already fixed earlier this PR: assert on _strip_fences()'s own output directly, which does distinguish "the block was stripped" from "_WAIVER_RE never matched a >-prefixed line in the first place". Confirmed live: this version fails without the `>` alternative present. Refs #1714 pytest: 50/50 passed. ruff/mypy clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7HpPtaimtUthJqyST8qx
Independent review verdict (archived -- round ending at commit 2529c6e)
Outer layer (GitHub-native reviewer). GitHub Copilot review requested ( Inner layer (reviewing-an-artifact, deferred per its own Step 0 to Round 1. Verdict: well-formed and well-placed, with two disclosed findings. (1) Round 2 (verifying round 1's own fix, commit Round 3 (verifying round 2's own fix). Independently re-derived and confirmed the blockquote test now genuinely fails when the Why CLEAN. All findings across three rounds are either fixed and independently re-verified through mutation testing (not merely re-read), or explicitly disclosed and separated into a new issue (#1721) as genuinely out of this PR's own scope per its own Acceptance Criteria Map -- no dimension silently left unaddressed, and no finding carried forward unresolved. Generated by Claude Code |
Brings in 43 commits from origin/main, including PR #1715's fix for the shared has_duplicate_waiver() pytest failure (issue #1714) and a new drafting-a-skill --strict-token-budget feature (evaluating-skill- quality: code-span and token-budget shape checks). Real content conflicts resolved (11 files): - evals/drafting-a-skill/eval-status.md, skills/drafting-a-skill/ SKILL.md, skills/drafting-a-skill/references/{gitapex-cross-links, guidance-form-and-sdo}.md: reconciled this branch's own issue #1648 three-dispatch-context redivision (contexts 1/2/3, the mkdir-EEXIST Step 2 bullet) with origin/main's independent --strict-token-budget feature (Step 6's new trim-priority order, escalate-on-exhausted- budget rule, and the token-budget-trim-priority.yaml fixture) -- both kept, neither one discarded for the other. - skills/drafting-a-skill/SKILL.md exceeded the new 5000-token budget once merged (8581 tokens); extracted the decision-log's resume-time ground-truth-drift and concurrent-dispatch-race detail into a new references/decision-log-discipline.md, per Step 6's own new trim- priority rule -- 7904 tokens, still over the advisory-only bar but the repo's own pre-commit invocation never passes --strict-token- budget against drafting-a-skill's own file (only against a skill it drafts), so this is not a blocking regression. - skills/executing-a-branch-plan/references/{decomposition-and- dispatch,events-and-review-gate,threat-model-and-authorization}.md: kept this branch's own restructured 4-reference-file layout (Round 10-14's own progressive-disclosure work) over origin/main's edits to the now-deleted execution-and-dispatch.md/refactor-and-review- gate.md/task-decomposition.md/domain-events-and-failure-handling.md/ code-quality-principles.md; ported the one real content change (docs/superpowers/plans/ -> docs/gitapex/plans/ naming-convention update) into decomposition-and-dispatch.md. - skills/scorer-gated-skill-edits/SKILL.md: kept this branch's own Round 1-9 concurrency-safety-hardened Procedure (worktree isolation, runner-firsthand-pinning extraction, drafting-a-skill dispatch for Step 3) over origin/main's much shorter, pre-hardening predecessor. Also fixed while verifying the merge (pre-existing, not introduced by this merge -- confirmed against the unmerged branch tip and origin/main directly before fixing): - 9 code-span-integrity shape-check failures (a `` ` ``-span split across a hard-wrap line break) across decomposition-and-dispatch.md, events-and-review-gate.md, threat-model-and-authorization.md, runner-firsthand-pinning.md, and evaluating-skill-quality's own adversarial-self-audit.md -- the same authoring-defect class Round 13 already found and fixed once in this branch's own history. - token-budget-trim-priority.yaml's own `exercises` fixture-coverage declaration hardcoded a truncated literal prefix of origin/main's own now-superseded two-line Step 6 heading; repointed to the stable `step 6` ordinal label and corrected a stray gate-script-name typo in its own comment. - docs/skill-eval-status.md regenerated (drafting-a-skill: 8 -> 9 fixtures) to match the corrected eval-status.md. Verified post-merge: shape checker 63/63 (drafting-a-skill), 63/63 (executing-a-branch-plan), 54/54 (scorer-gated-skill-edits); drift scanner unchanged (same 4 pre-existing, confirmed-unrelated findings on executing-a-branch-plan, none on the other two); full pytest suite passing; all local-preflight gates passing. Refs #1630, #1648 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7HpPtaimtUthJqyST8qx
Summary
has_duplicate_waiver()inhooks/gitapex_check_pr_duplicate_issue.pymisdetected a fence-shapedDuplicate-PR-waiver:reason (e.g.~~~) as opening an unterminated code fence, silently stripping the reason before its own regex ever saw it. Ports the exact, already-adversarially-hardened fix issue #1432/PR #1440 applied to the sibling copy of this same defect class.Facts
has_duplicate_waiver("Some PR description.\n\nDuplicate-PR-waiver: ~~~\n")returnedFalse(expectedTrue), root-caused to_UNTERMINATED_FENCE_RE/_FENCE_REmatching a fence marker anywhere in the text rather than only at the start of its own line -- confirmed reproducing identically and deterministically onorigin/main(commit2ce6c2f5) via direct function call, independent of any PR.pytestCI check failure (aderandomize=TrueHypothesis test, the same failing example every run) on PR feat(drafting-a-skill): close Dimension 4-8 gaps + redivide edit responsibility #1632, which does not touch this file at all -- diagnosed there, disclosed via a PR comment, and filed here as issue fix(hooks): has_duplicate_waiver misdetects a fence-shaped waiver reason as an unterminated code fence #1714 rather than widening that PR.skills/drafting-issues/scripts/gitapex_check_acm_present.py's own_FENCE_RE/_UNTERMINATED_FENCE_RE, which this file's own copies previously mirrored verbatim) -- that fix's own two adversarial-review rounds additionally found and closed a list-item/blockquote-prefixed-fence inversion bug and a ReDoS shape (CWE-1333) a naive line-start anchor alone would have reintroduced. This PR ports that same, already-hardened_CONTAINER_PREFIXconstruction rather than re-deriving it.Assumptions
Risk / blast radius
Scoped to
hooks/gitapex_check_pr_duplicate_issue.pyand its own test file only. This hook only guardsmcp__github__create_pull_request(denying a duplicate-issue PR without a waiver) -- widening what counts as a valid waiver reason is a narrowing-the-false-positive-rate change, not a new false-negative: every previously-detected waiver and every previously-stripped genuine fenced block is re-verified unchanged by the existing test suite plus new regression tests.Rollback
Revert this PR's merge commit; no schema, data, or external-service change.
Verification
Restating issue #1714's own Acceptance Criteria Map:
has_duplicate_waivercorrectly recognizes aDuplicate-PR-waiver:line's own disclosed reason even when that reason starts with or contains fence-marker characters that do not actually open a real fence, and every already-adversarially-verified case from issue #1432/PR #1440's own fix holds identically_CONTAINER_PREFIX-based construction from PR #1440 into this file's own_FENCE_RE/_UNTERMINATED_FENCE_RE; regression tests mirroring that fix's own defeat-test set (fence-shaped reason, list-item/blockquote/ordered/+-fenced rejection, bare-CR-fenced rejection, ReDoS wall-clock budget), strengthened across Step 8's own three review rounds below after two were found vacuous and fixeduv run --frozen python3 -m pytest -q --no-cov hooks/test_gitapex_check_pr_duplicate_issue.py tests/test_gitapex_check_pr_duplicate_issue_properties.py: 50/50 passed, including the previously-failing property test.ruff check/ruff format --check/mypy: clean. All 45gitapex_gate_local_preflight.pygates passed on commit2529c6e4.Independent review verdict
Outer layer (GitHub-native reviewer). GitHub Copilot review re-requested (
request_copilot_review) at approximately 2026-09-03T10:37Z. No response posted at request time. Per the operator's explicit instruction, the 30-minute wait for this round was skipped rather than observed -- disclosed as unreachable/skipped for this run, not silently omitted.Inner layer (reviewing-an-artifact, deferred per its own Step 0 to
evaluating-deterministic-gate-quality). This PR's diff is unchanged since the prior round -- still entirely a deterministic gate and its own test file (hooks/gitapex_check_pr_duplicate_issue.py, registered in.gitapex/ssot.jsonas gate idpr-duplicate-issue) -- so Step 0 again deferred to that specialist. A fresh, isolatedAgentdispatch (no memory of the prior rounds, general-purpose subagent with Bash access for live testing) independently re-derived the verdict from the current code rather than trusting the prior round's own recorded result.Verdict: well-formed and well-placed. Confirmed the gate is correctly wired (
.gitapex/ssot.json:279-293,hooks/hooks.json:71, matchermcp__github__create_pull_request) and that this diff only changes the fence-detection regexes inside an already-correctly-placed gate.Live-tested directly against the checked-out module (not read-only inference):
pytest hooks/test_gitapex_check_pr_duplicate_issue.py tests/test_gitapex_check_pr_duplicate_issue_properties.py: 50 passed, target module at 100% line coverage.+-marker fences: fabricated interior waivers correctly stripped; real waivers outside such blocks still detected.re.MULTILINEanchor semantics confirmed correct across multi-line spans.No bypass (fail-open) scenario found; the targeted bug class is closed.
Finding (non-blocking, disclosed, not fixed in this round).
_CONTAINER_PREFIXand its two dependent regexes now exist verbatim in two files (hooks/gitapex_check_pr_duplicate_issue.pyandskills/drafting-issues/scripts/gitapex_check_acm_present.py) with no automated parity/sync test. This is a pre-existing pattern this PR ports rather than a regression it introduces, and the reviewing agent explicitly characterized it as not blocking. Not treated as a confirmed finding requiring a Step 3 loop-back; recorded here as disclosure for a human reader. A small parity test (asserting the two_CONTAINER_PREFIXvalues stay identical) is a reasonable follow-up, potentially in scope for issue #1721 (already filed for the sibling-file drift) or a new issue.Why CLEAN. Zero confirmed findings against the current diff from either layer. The one disclosed finding is explicitly non-blocking and pre-existing, not a defect in this PR's own change.
pytestis green. See the archived prior round (PR comment) for the full three-round mutation-testing account that closed the original bug class.Skill audit evidence
hooks/gitapex_check_pr_duplicate_issue.pyis registered as a deterministic gate (.gitapex/ssot.jsongates[].script). Read this diff againstskills/evaluating-deterministic-gate-quality/references/dimensions.mddimension 15 (fail-closed default on incomplete or malformed input) specifically, per that dimension's own instruction not to credit it from the bundled test suite's happy-path coverage alone. Independently constructed and ran, live, directly against the current code rather than from memory:has_duplicate_waiver(None)->False(no crash, falls through to the normal duplicate-citation check rather than silently allowing); a malformed-JSON stdin payload ->error: payload is not valid JSON..., exit 1; a resolving-citation payload withGH_TOKEN/GITHUB_TOKENunset ->FAIL: cannot verify #1 ... failing closed, exit 1; a wrong-typedbodyfield (int) ->error: ... field 'body' must be a string ..., exit 1; a pathological 200-space-padded unterminated-fence input against the new/changed_FENCE_RE/_UNTERMINATED_FENCE_RE-> completed in 0.0001s (no ReDoS). Every case denies or safely no-ops rather than silently allowing. This PR's own diff changes only the fence-detection regexes and their own test coverage insidehas_duplicate_waiver; the pagination-bound, missing-token, and malformed-payload fail-closed paths verified above are all pre-existing and unmodified by this diff. See## Independent review verdictabove for the full three-round mutation-testing account this dimension's own live-testing requirement was additionally satisfied by._CONTAINER_PREFIX/_WAIVER_REbranch this file's own tests claim to cover was independently mutation-tested in Step 8's own Round 3 (see## Independent review verdictabove): each of six targeted mutants broke exactly one branch, and exactly the test(s) naming that branch failed -- proof the defeat tests genuinely defeat the logic they claim to, not merely exercise its happy path. Two tests (the bare-CR and blockquote cases) were found vacuous during this same process and rewritten to assert on_strip_fences()'s own output directly rather than throughhas_duplicate_waiver()alone, closing the gap a first pass had missed.Checklist
skills/*/SKILL.md, adocs/superpowers/specs/*.mddesign doc, a security-relevant skill, or a deterministic checker script underskills/*/scripts/*.py,evals/scripts/*.py, or.github/scripts/*.py(the checklist's own enumerated scope) -- this PR's own files are underhooks/, buthooks/gitapex_check_pr_duplicate_issue.pyis independently registered as a deterministic gate (.gitapex/ssot.json), so the## Skill audit evidencesection above disclosesdeterministic-gate-qualityanddefeat-test-disclosureas requiredevals/*/split.mdKept-edit-log entry addedskills/*/SKILL.mdStop-boundary/branch changeMerge gate: independent review
This PR is also subject to the
independent-review-pendingrequiredstatus check (see
.github/workflows/independent-review-pending.yml/.github/scripts/gitapex_gate_independent_review_pending.py). It stayspending/failing until a
## Independent review verdictsection namingthis PR's current head commit is recorded in this body --
drafting-a-pr-to-merge's own Step 8 records it once its independentreview completes. There is nothing for you to do here now: do not
pre-fill this section yourself, and do not remove this note.
Related Issue
Closes #1714