Skip to content

Reject acceptance criteria with no provable finish line - #638

Merged
thomasluizon merged 4 commits into
mainfrom
chore/finish-line-gate
Jul 28, 2026
Merged

Reject acceptance criteria with no provable finish line#638
thomasluizon merged 4 commits into
mainfrom
chore/finish-line-gate

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Problem

PR #633 (ORB-122) has taken 24 review rounds over 12 hours and has not converged. PR #625 (ORB-106) is at 32 reviews. Neither is a bad worker or a noisy reviewer: I read the review that un-approved #633 and it is a real HIGH, traced against the actual blob with a working bypass string and a two-line fix.

The cause is the ticket. ORB-122 asks the worker to block every phrasing an agent could emit that surfaces a raw repo command. That is not a set anyone can enumerate, so every round the reviewer legitimately finds one more member of it and is correct. There is no state of the code that satisfies the criterion, so review cannot terminate.

Its last two commit headlines say it plainly: cover node invocation variants, close remaining surfacing gaps.

What this adds

check-ticket.mjs now rejects an acceptance criterion that:

  • quantifies over an open set (every/all/any/each with nothing in the same criterion bounding it), or
  • trails off into an unnamed remainder (etc., and so on, or similar, an ellipsis).

A criterion is bounded when it names its enumeration: a count (digits or one..ten), a file path, a backticked command, or an explicit listed/named/in the table reference. So this stays legal and is better written:

  • every phrasing rejected by + "node tools/check-ticket.mjs" + is blocked

and this is rejected:

  • every phrasing a worker could emit is blocked

Where it fires

No new checkpoint. The gate already runs everywhere a ticket passes: /ticket and /feature on the draft and again on the created issue, /orchestrate per ticket at wave-plan time (so it catches tickets written before this rule existed), and the four /audit-* skills plus /prod-readiness on what they propose.

What it actually buys, measured

Run against all 20 harness ticket bodies in the current run, correlated with each PR's review count:

Ticket claude reviews Gate
ORB-106 32 rejects
ORB-122 24 rejects
ORB-129 33 misses
ORB-113 12 misses
ORB-112 2 rejects (demands a citation)
ORB-115 3 rejects (demands a citation)

It catches two of the four non-converging tickets and misses two. ORB-129 and ORB-113 stalled for other reasons (a screen-scraping predicate over a TUI, and a merge conflict), not unbounded criteria, so no wording rule would have caught them. The two low-review tickets it stops are not false alarms in kind: both say "every X" without naming where X is enumerated, and the fix is one edit that makes the criterion checkable.

This is a partial gate, deliberately shipped as one. It does not claim to detect every unbounded ticket, which would be the same mistake the gate exists to prevent.

Gates

node tools/test-tools.mjs          -> ORBIT TOOLS GATE OK
node .claude/hooks/test-hooks.mjs  -> ORBIT HOOK PARITY OK

Three new cases in tools/test-tools.mjs: the open-set rejection, the same criterion passing once it names its deciding command, and the unnamed-remainder rejection.

🤖 Generated with Claude Code

https://claude.ai/code/session_01G3TtwQhtWZdAndmpfmf8rC

check-ticket.mjs now fails an acceptance criterion that quantifies over an
open set (every/all/any/each with nothing in the same criterion bounding it)
or trails off into an unnamed remainder (etc., and so on, ellipsis).

Measured cause: ORB-122 (PR #633) took 24 review rounds and 12 hours because
"block every phrasing an agent could emit" is not a set anyone can enumerate,
so every round the reviewer legitimately found one more member and was right.
ORB-106 (PR #625) shows the same shape at 32 reviews. A criterion that names
its enumeration (a count, a named list, a file, or a backticked command) has a
finish line; one that does not cannot be proven done by anyone.

The gate runs at every point a ticket already passes through: /ticket and
/feature on the draft, /orchestrate again per ticket at wave-plan time, and
the four /audit-* skills plus /prod-readiness on what they propose.

node tools/test-tools.mjs      -> ORBIT TOOLS GATE OK
node .claude/hooks/test-hooks.mjs -> ORBIT HOOK PARITY OK

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3TtwQhtWZdAndmpfmf8rC
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Jul 28, 2026 2:54pm

Request Review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #638 — Reject acceptance criteria with no provable finish line

Scope: PR #638 in thomasluizon/orbit-ui-mobile — tools/check-ticket.mjs, tools/test-tools.mjs
Recommendation: APPROVE

Severity Count
Critical (incl. ⚠️ old-client breaks) 0
High 0
Medium 0
Low / Info 2

Summary

Adds two regex-driven checks to check-ticket.mjs's acceptance-criteria validation: reject a criterion that quantifies over an open set (every/all/any/each) unless the same line names what bounds it (a count, a backticked command/file, an extension, or listed/named/above/below/in the table), and reject a criterion that trails into an unnamed remainder (etc., and so on, an ellipsis). Both new decision paths get dedicated test cases in tools/test-tools.mjs, and CI's Harness Execution job (which runs node tools/test-tools.mjs + node .claude/hooks/test-hooks.mjs) is green on this PR. The change is small, self-contained, and matches its stated intent with no regressions to the existing gate's behavior.

Findings

Critical

None.

High

None.

Medium

None.

Low / Info

[INFO] Multi-line criteria items are only checked on their first line
· dimension: 1. Correctness
· location: tools/check-ticket.mjs:66-69
· issue: `criteriaItems` is extracted via /^[ \t]*(?:[-*]|\d+\.)[ \t]+.*$/gm, which captures only the bullet's own line. If a criterion wraps onto a continuation line without a new marker, an "every X, etc." tail or an unbounded quantifier on that continuation line is invisible to both new checks.
· risk: Low in practice — every criterion in the repo's own fixtures (VALID_TICKET_BODY, the 6.2 template) is single-line, and this matches the prior counting behavior (the old regex also only matched the marker line). Worth a one-line note if multi-line criteria ever become idiomatic.
· fix: no action needed now; if multi-line bullets appear, extend the item regex to consume continuation lines (e.g. up to the next marker or blank line).
· reference: CLAUDE.md rule 1 (root cause) — informational only, not a defect in this diff.
[INFO] Quantifier words used in a domain-bounded (not enumerable-set) sense will be rejected without a marker
· dimension: 1. Correctness
· location: tools/check-ticket.mjs:48-51
· issue: BOUNDED_BY requires a digit, a spelled-out number, a backticked span, a file extension, or an explicit "listed/named/above/below/in the table" cue. A criterion like "the button disables when all fields are filled" is bounded by domain context (a fixed, known form) but carries none of those markers, so it would be rejected and need rewording (e.g. add a digit or backtick).
· risk: Adds authoring friction on some criteria that are not actually open-set — a false positive, not a false negative. No concrete example found of this breaking an existing committed ticket template or fixture (searched .claude/skills/ticket, _shared/audit-to-tickets.md — no matching wording), so this is a design-tradeoff observation, not a reproduced defect.
· fix: none required; the PR body already frames this as a deliberately partial gate with measured hit/miss data. If false positives prove common in practice, widen BOUNDED_BY (e.g. a domain-noun list) rather than loosening the quantifier match.
· reference: PR #638 body, "This is a partial gate, deliberately shipped as one."

Subagents

Agent Verdict
parity-checker N/A — no apps/web/** or apps/mobile/** file changed
i18n-syncer N/A — no user-facing strings or i18n JSON changed
contract-aligner N/A — no packages/shared/src/types/* or endpoints.ts changed; single-repo diff
security-reviewer N/A — no orbit-api code changed (sibling repo also not checked out)
design-reviewer N/A — no apps/* or orbit-landing-page UI files changed

Validation

Check Result
Lint PASS (CI: PR Tests / Lint, SUCCESS)
Type check PASS (CI: PR Tests / Type Check, SUCCESS)
Build PASS (CI: PR Tests / Build, SUCCESS)
Tests / Harness Execution (node tools/test-tools.mjs + node .claude/hooks/test-hooks.mjs) PASS (CI: Guards / Harness Execution, SUCCESS)
SonarCloud PASS (SUCCESS)
Dash Ban / Copy Register / Suppressions Ratchet / Cross-Platform Parity / Skill and Agent Frontmatter / Expo SDK Pin / Harness Lockstep all PASS (Guards, SUCCESS)

Per task instructions, Phase 6 (/validate) was skipped — Build/Unit Tests/SonarCloud are covered by required CI checks, cited above as PASS.

Deferred — N/A dimensions & files not verdicted

  • Dimension 8 (DESIGN.md / AI-slop): N/A — diff touches no apps/* UI files.
  • Dimension 9 (Parity): N/A — diff touches no apps/web/** or apps/mobile/**.
  • Dimension 10 (i18n): N/A — no user-facing strings or i18n JSON touched.
  • Dimension 11 (Contract drift + backward-compat): N/A — no packages/shared/src/types/*, no orbit-api DTOs in this diff. The orbit-api side of the backward-compat guard is not verifiable in this CI job (sibling repo not checked out), but is moot regardless since this diff carries no contract changes.
  • Dimension 13 (Backend hard rules): N/A — orbit-api untouched (and not checked out in this session).
  • Dimension 14 (FEATURES.md parity): N/A — internal tooling change, no user-facing feature/screen/tool/gating surface changed.
  • Prior review threads: none exist on this PR (reviews: [], no inline comments) — nothing to reconcile against; this is a full first-pass review.

Both changed files (tools/check-ticket.mjs, tools/test-tools.mjs) received a full verdict; nothing in scope was left unverdicted.

What's good

  • The new checks are narrowly scoped, each backed by its own dedicated positive and negative test case (new decision paths get their own cases, not just extended existing ones).
  • The JSDoc comments on the new regex constants cite the concrete motivating incident (PR #633 / ORB-122, 24 review rounds) with real measured numbers rather than vague rationale.
  • The PR body is unusually honest about the gate's limits: it states plainly what it catches (2/4 non-converging tickets) and what it misses, rather than overclaiming completeness.
  • No regression risk to the existing >= 2 checkable items check — the extraction regex was widened to capture full lines but the counting semantics are preserved and covered by pre-existing tests.

Recommendation

Merge as-is. No Critical or High findings survive review. The two Info-level observations (multi-line criteria blind spot, occasional false-positive friction on domain-bounded quantifiers) are worth keeping in mind if ticket-writing patterns evolve, but neither is a defect in this diff and neither should block merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 953badf6f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/check-ticket.mjs
thomasluizon and others added 2 commits July 28, 2026 11:32
Review finding on PR #638: BOUNDED_BY searched the whole criterion, so an
unrelated token rescued an unbounded claim. Measured bypass, which the gate
accepted before this commit and rejects after it:

  "every phrasing a worker could emit is blocked and the command exits 1"

The stray 1 satisfied the bound while the unprovable finish line survived
untouched. The bound now has to appear in the same clause as the quantifier.
Code spans and file paths are masked before the split so a backticked command
containing dots is not cut in half by its own extension.

Six measured cases through the real tool, all correct: the bypass above and
"...blocked, see tools/check-ticket.mjs" reject; "every phrasing rejected by
`node tools/check-ticket.mjs` is blocked", "The three docs each carry a WHY
note" and "all 5 call sites route through the shared reader" pass; "any single
check failing produces a FAIL row" rejects.

node tools/test-tools.mjs      -> ORBIT TOOLS GATE OK
node .claude/hooks/test-hooks.mjs -> ORBIT HOOK PARITY OK

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3TtwQhtWZdAndmpfmf8rC
@thomasluizon

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: da82721a49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Complete

Scope: PR #638 in thomasluizon/orbit-ui-mobile — tools/check-ticket.mjs, tools/test-tools.mjs
Recommendation: APPROVE

Prior review context: This same reviewer (claude) already approved commit 953badf6 (0 Critical/High, 2 Info). Since then, one new commit landed — da82721a "fix(tools): tie the bound to the clause its quantifier is in" — which is what this pass reviewed. (An intervening merge-main commit, 7d2896f1, pulled in unrelated ORB-129 test-tools.mjs changes from main; those are not part of this PR's diff and were excluded from review.)

What the new commit does: BOUNDED_BY previously searched the whole criterion string, so an unrelated token elsewhere in the sentence could rescue an unbounded quantifier claim — e.g. "every phrasing a worker could emit is blocked and the command exits 1" passed only because of the stray 1. The fix adds quantifierClauses(), which masks backtick spans and file-extension paths first (so a backticked command's own dots don't get split), then splits the criterion into clauses on . ; : , / and / or, and requires every clause that contains a quantifier word to independently satisfy BOUNDED_BY. One new regression test (tools/test-tools.mjs) directly covers the fixed bypass and asserts status 1 / quantifies over an open set.

Hand-traced against all six scenarios the commit message claims are correct (the bypass, a comma-separated "see file" reference, a backticked-command bound, two spelled-out-number bounds, and an "any" with no bound) — all matched the claimed behavior. One theoretical edge case exists (a quantifier word that exists only inside a masked backtick/file span vacuously passes .every() on an empty clause list) but it is not a regression — the pre-fix code also passed that shape via the same backtick match — and no realistic ticket-writing pattern reaches it, so it does not rise to a reportable finding.

Severity Count
Critical 0
High 0
Medium 0
Low / Info 0

Subagents: parity-checker, i18n-syncer, contract-aligner, security-reviewer, design-reviewer — all N/A (diff touches only tools/*.mjs, no apps/, packages/shared/src/types, or orbit-api).

Validation: All completed CI checks are SUCCESS (Lint, Type Check, Build, Dash Ban, Copy Register, Suppressions Ratchet, Cross-Platform Parity, Expo SDK Pin, Skill/Agent Frontmatter, Harness Lockstep, Contract Drift, CodeQL, GitGuardian, Dependency Review/Audit, Architecture map drift). Harness Execution, Unit Tests, and SonarCloud Analysis run in parallel as separate required checks per this CI job's instructions and were not re-validated locally in this pass.

Deferred: Dimensions 8–14 (DESIGN.md, Parity, i18n, Contract drift/backward-compat, Security, Backend hard rules, FEATURES.md parity) — N/A, no matching surface in this diff. The orbit-api side of the backward-compat guard is not verifiable in this CI job (sibling repo not checked out), but is moot since this diff carries no contract changes.

…ed compounds

Two precision defects found by running the gate against the three ticket
bodies still queued in this run, before their workers start.

ORB-107: "A clean environment produces an all-PASS table and exit 0" was
rejected because "all-PASS" is a hyphenated compound adjective, not a
quantifier. The quantifier now requires no following hyphen.

ORB-120: "`design-specialist.md` and `product-manager.md`, which currently
declare no `model:`, each gain one" was rejected because the two named files
sit in clauses BEFORE the one holding "each". A bound now counts when it
appears in the quantifier's own clause or any clause preceding it, never in a
clause after it, which is exactly the direction the PR #638 review bypass
exploited ("...is blocked and the command exits 1").

Verdicts after this commit, via --issue against the live bodies:
ORB-89 clean, ORB-107 "ticket ok", ORB-120 clean.

node tools/test-tools.mjs      -> ORBIT TOOLS GATE OK
node .claude/hooks/test-hooks.mjs -> ORBIT HOOK PARITY OK

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3TtwQhtWZdAndmpfmf8rC
@sonarqubecloud

Copy link
Copy Markdown

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #638

Recommendation: APPROVE

Summary

tools/check-ticket.mjs gains a heuristic that rejects an acceptance criterion quantifying over an open set (every/all/any/each with no in-clause bound) or trailing into an unnamed remainder (etc., ..., "or similar"). The diff is tooling-only (tools/check-ticket.mjs, tools/test-tools.mjs) and touches no apps/*, packages/shared, or orbit-api surface. It ships with four new hermetic test-tools.mjs cases covering every new decision path. The PR's own commit history shows two fixup commits (da82721a, 832c35c8) that already closed a real review bypass ("...is blocked and the command exits 1") and a hyphenated-compound false positive (all-PASS) found while this PR was under review. Hand-tracing the regex logic against both the new and existing fixtures found no surviving Critical/High defect.

Findings

Critical / High / Medium

None.

Low / Info

  • Intentional, disclosed tradeoff, not an oversight: isBounded (tools/check-ticket.mjs:60-68) only accepts a bound in the quantifier's own clause or a clause before it, never after. A criterion like "every scenario is covered: A, B, C, three total" still gets flagged, because the enumeration trails the quantifier's clause. This is the deliberate fix (832c35c8) for the exact bypass direction this PR's own review found, and the rejection message already tells the author the fix (state the bound in-clause, or backtick it). No action needed.

Subagents

N/A across the board — no apps/*, packages/shared types, or orbit-api file changed, so parity-checker / i18n-syncer / contract-aligner / security-reviewer / design-reviewer all sit outside their gate.

Validation

Lint / Type Check / Unit Tests all PASS per the PR's own CI rollup (PR Tests workflow, run 30370677394). Guards / Harness Execution PASS (run 30370677100, job 90313392105). This session's sandbox blocked a local re-run of node tools/test-tools.mjs / node .claude/hooks/test-hooks.mjs (arbitrary script execution needs interactive approval unavailable here), so validation is sourced from the PR's green statusCheckRollup plus the author's own gate output quoted in the PR body.

Deferred

DESIGN.md/AI-slop (#8), Parity (#9), i18n (#10), Contract drift (#11), Backend hard rules (#13), FEATURES.md parity (#14): all N/A, no matching surface in the diff. Type safety (#6): N/A in the strict TS sense (tools/** is plain untyped .mjs outside the ESLint-covered workspaces); no any/as any/unjustified null! present regardless. Both changed files were read in full and given a verdict.

What's good

  • Self-aware about its own limits: names the two tickets its heuristic misses (ORB-129, ORB-113) and why, instead of overclaiming completeness.
  • Every new decision path gets its own test-tools.mjs case per tools/CONVENTIONS.md / rubric dimension 15.
  • Already shows one full review-and-fix cycle against itself in its commit history, and the fixes are correctly reasoned.

Recommendation

Merge as-is. No action items.

@thomasluizon
thomasluizon merged commit b707c33 into main Jul 28, 2026
30 checks passed
@thomasluizon
thomasluizon deleted the chore/finish-line-gate branch July 28, 2026 15:29
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.

1 participant