Skip to content

fix(hooks): anchor fence-marker detection to line start in has_duplicate_waiver - #1715

Merged
tvna merged 4 commits into
mainfrom
claude/fix-duplicate-waiver-fence-1714
Sep 3, 2026
Merged

fix(hooks): anchor fence-marker detection to line start in has_duplicate_waiver#1715
tvna merged 4 commits into
mainfrom
claude/fix-duplicate-waiver-fence-1714

Conversation

@tvna

@tvna tvna commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

has_duplicate_waiver() in hooks/gitapex_check_pr_duplicate_issue.py misdetected a fence-shaped Duplicate-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

Assumptions

Risk / blast radius

Scoped to hooks/gitapex_check_pr_duplicate_issue.py and its own test file only. This hook only guards mcp__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:

Criterion Proof method Result
has_duplicate_waiver correctly recognizes a Duplicate-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 Ported the exact _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 fixed Done. uv 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 45 gitapex_gate_local_preflight.py gates passed on commit 2529c6e4.

Independent review verdict

  • Verdict: CLEAN
  • Verified commit: 2529c6e

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.json as gate id pr-duplicate-issue) -- so Step 0 again deferred to that specialist. A fresh, isolated Agent dispatch (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, matcher mcp__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.
  • Mid-line (non-line-start) fence-shaped waiver reason: correctly detected as a real waiver (not stripped).
  • Genuine line-start fence containing a fabricated waiver: correctly stripped.
  • Backreference cross-type check (open ``` / close ~~~): correctly treated as unterminated, not falsely paired.
  • List-item, blockquote, ordered-marker, and +-marker fences: fabricated interior waivers correctly stripped; real waivers outside such blocks still detected.
  • ReDoS stress tests beyond the PR's own bundled case, across 5 adversarial payload shapes up to ~40KB: no catastrophic-backtracking behavior found in any.
  • re.MULTILINE anchor 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_PREFIX and its two dependent regexes now exist verbatim in two files (hooks/gitapex_check_pr_duplicate_issue.py and skills/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_PREFIX values 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. pytest is 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

  • deterministic-gate-quality: RAN -- hooks/gitapex_check_pr_duplicate_issue.py is registered as a deterministic gate (.gitapex/ssot.json gates[].script). Read this diff against skills/evaluating-deterministic-gate-quality/references/dimensions.md dimension 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 with GH_TOKEN/GITHUB_TOKEN unset -> FAIL: cannot verify #1 ... failing closed, exit 1; a wrong-typed body field (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 inside has_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 verdict above for the full three-round mutation-testing account this dimension's own live-testing requirement was additionally satisfied by.
  • defeat-test-disclosure: RAN -- every _CONTAINER_PREFIX/_WAIVER_RE branch this file's own tests claim to cover was independently mutation-tested in Step 8's own Round 3 (see ## Independent review verdict above): 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 through has_duplicate_waiver() alone, closing the gap a first pass had missed.

Checklist

  • Tests pass locally
  • Docs updated if behavior changed
  • Issue number cited in every commit
  • If this PR adds/modifies a skills/*/SKILL.md, a docs/superpowers/specs/*.md design doc, a security-relevant skill, or a deterministic checker script under skills/*/scripts/*.py, evals/scripts/*.py, or .github/scripts/*.py (the checklist's own enumerated scope) -- this PR's own files are under hooks/, but hooks/gitapex_check_pr_duplicate_issue.py is independently registered as a deterministic gate (.gitapex/ssot.json), so the ## Skill audit evidence section above discloses deterministic-gate-quality and defeat-test-disclosure as required
  • N/A: no evals/*/split.md Kept-edit-log entry added
  • N/A: no skills/*/SKILL.md Stop-boundary/branch change

Merge gate: independent review

This PR is also subject to the independent-review-pending required
status check (see .github/workflows/independent-review-pending.yml /
.github/scripts/gitapex_gate_independent_review_pending.py). It stays
pending/failing until a ## Independent review verdict section naming
this PR's current head commit is recorded in this body --
drafting-a-pr-to-merge's own Step 8 records it once its independent
review 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

…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.
@tvna
tvna temporarily deployed to ruleset-verify September 3, 2026 05:06 — with GitHub Actions Inactive
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 25761cab-5fc2-4ec8-8abd-ba1b241566cf


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.67%. Comparing base (45e265e) to head (2529c6e).
⚠️ Report is 17 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…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
@tvna
tvna temporarily deployed to ruleset-verify September 3, 2026 09:29 — with GitHub Actions Inactive
@tvna
tvna marked this pull request as draft September 3, 2026 09:37

tvna commented Sep 3, 2026

Copy link
Copy Markdown
Owner Author

Independent review verdict (archived -- round ending at commit 2529c6e)

  • Verdict: CLEAN
  • Verified commit: 2529c6e

Outer layer (GitHub-native reviewer). GitHub Copilot review requested (request_copilot_review) at approximately 2026-09-03T05:30Z. No response posted within the 30-minute window (checked well past that window via get_reviews: empty). Per drafting-a-pr-to-merge Step 8's own rule, treated as unreachable for this run -- disclosed, not silently omitted.

Inner layer (reviewing-an-artifact, deferred per its own Step 0 to evaluating-deterministic-gate-quality). This PR's diff is entirely a deterministic gate and its own test file (hooks/gitapex_check_pr_duplicate_issue.py, registered in .gitapex/ssot.json), so Step 0 deferred the full review to that specialist rather than running the ordinary five-persona fan-out. Three isolated dispatches (fresh Agent subagent, no memory of authoring this diff, working from a snapshot directory outside this repository's own CLAUDE.md/AGENTS.md ancestry, Read/Grep/Glob/synthetic-local-Bash only) ran across this Step 8 round, each verifying the prior round's own fix from scratch rather than only re-confirming its claim -- the same discipline this PR's own sibling PR #1632 established this session.

Round 1. Verdict: well-formed and well-placed, with two disclosed findings. (1) 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 normalization, not the regex branch it named -- live-confirmed by rebuilding _CONTAINER_PREFIX without the \r alternative and observing the test still pass. (2) A sibling module this hook imports from, hooks/gitapex_check_pr_issue_acm_disclosure.py, still carries the pre-fix, line-start-unanchored _FENCE_RE -- a real, currently-live instance of the same bug class in a different file/function (citation extraction, not waiver detection), confirmed by direct read. Fixed (in scope): corrected the bare-CR test's docstring and added test_strip_fences_rejects_a_bare_cr_fenced_block_without_normalization, calling _strip_fences() directly to genuinely pin the \r branch -- confirmed live to fail without that branch present. Also added a blockquote-fence regression test to close a coverage gap the same round flagged (the > alternative was live-verified correct but untested). Disclosed, not fixed here (out of scope): filed issue #1721 for the sibling-file drift, per issue #1714's own Acceptance Criteria Map scoping this PR to has_duplicate_waiver() alone -- the same relationship issue #1432 already has to issue #1714 (distinct instances of one bug class, each its own issue).

Round 2 (verifying round 1's own fix, commit d2f0126e). The bare-CR fix was independently re-derived and confirmed closed. The new blockquote test, however, was itself found defective: it asserted only through has_duplicate_waiver(), but that function's own _WAIVER_RE line-prefix (^[ \t]*[-*]?[ \t]*) never matches a >-prefixed line at all, regardless of whether _strip_fences() correctly handles a blockquote-fenced block -- so the test passed vacuously. Live-confirmed by removing the > alternative from _CONTAINER_PREFIX entirely and observing the test still pass. Fixed: rewrote the test to assert on _strip_fences()'s own output directly (matching the pattern the bare-CR fix already established), confirmed locally to fail without the > branch present before pushing (commit 2529c6e4).

Round 3 (verifying round 2's own fix). Independently re-derived and confirmed the blockquote test now genuinely fails when the > branch is removed (a full mutation-tested run: 1 failed, 44 passed, the one failure at exactly the asserted line). Went further than a single re-check: mutation-tested every other claimed _CONTAINER_PREFIX/_WAIVER_RE branch this PR's own tests name (list bullets -/*, ordered 1., +, bare-CR \r, _WAIVER_RE's optional bullet, its \S reason-required guard) -- six separate mutants, each breaking exactly one targeted branch, with exactly the test(s) naming that branch failing and zero collateral or silent passes. No further vacuous test found anywhere in the file. Full suite: 45/45 passed.

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. skill-audit-disclosure (see ## Skill audit evidence below) is populated and green. pytest is green (the previously-failing property test now passes). All 45 gitapex_gate_local_preflight.py gates pass on commit 2529c6e4.


Generated by Claude Code

@tvna
tvna marked this pull request as ready for review September 3, 2026 11:25
@tvna
tvna merged commit f8dadcf into main Sep 3, 2026
45 of 46 checks passed
@tvna
tvna deleted the claude/fix-duplicate-waiver-fence-1714 branch September 3, 2026 11:28
tvna pushed a commit that referenced this pull request Sep 3, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(hooks): has_duplicate_waiver misdetects a fence-shaped waiver reason as an unterminated code fence

2 participants