Skip to content

ORB-141 Fix merge sweep review lookup - #646

Merged
thomasluizon merged 2 commits into
mainfrom
feature/orb-141-the-merge-sweep-can-never-merge
Jul 28, 2026
Merged

ORB-141 Fix merge sweep review lookup#646
thomasluizon merged 2 commits into
mainfrom
feature/orb-141-the-merge-sweep-can-never-merge

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

ORB-141

Summary

  • Removed the unsupported --slurp flag from the paginated review GraphQL lookup in both merge sweep twins.
  • Changed the jq selector to transform each page from the response root while preserving the author, timestamp, and URL stream.
  • Added a static harness guard that reads both real script filenames, fails if either file is missing, and rejects --slurp combined with --jq or --template.
  • Updated the existing pagination assertion to require --paginate and forbid --slurp.

Root cause and impact

GitHub CLI 2.88.1 rejects --slurp whenever --jq or --template is present. The review lookup therefore always exited nonzero, and the correct fail-closed behavior skipped every pull request. The fix removes only the impossible invocation. Review activity definitions, cutoff semantics, malformed response handling, and fail-closed behavior are unchanged.

Live GitHub CLI evidence

Executed against the real GitHub API and PR #642.

Before:

gh api graphql --paginate --slurp \
  -f query='query($o:String!,$r:String!,$n:Int!,$endCursor:String){repository(owner:$o,name:$r){pullRequest(number:$n){reviews(first:100,after:$endCursor){nodes{author{login} submittedAt updatedAt lastEditedAt url} pageInfo{hasNextPage endCursor}}}}}' \
  -F o=thomasluizon -F r=orbit-ui-mobile -F n=642 \
  --jq '.[] | .data.repository.pullRequest.reviews.nodes[] | ([.author.login, .submittedAt, .url], [.author.login, .updatedAt, .url], [.author.login, .lastEditedAt, .url]) | select(.[1] != null) | @tsv'

the `--slurp` option is not supported with `--jq` or `--template`
exit 1

After:

gh api graphql --paginate \
  -f query='query($o:String!,$r:String!,$n:Int!,$endCursor:String){repository(owner:$o,name:$r){pullRequest(number:$n){reviews(first:100,after:$endCursor){nodes{author{login} submittedAt updatedAt lastEditedAt url} pageInfo{hasNextPage endCursor}}}}}' \
  -F o=thomasluizon -F r=orbit-ui-mobile -F n=642 \
  --jq '.data.repository.pullRequest.reviews.nodes[] | ([.author.login, .submittedAt, .url], [.author.login, .updatedAt, .url], [.author.login, .lastEditedAt, .url]) | select(.[1] != null) | @tsv'

claude	2026-07-28T18:24:02Z	https://github.com/thomasluizon/orbit-ui-mobile/pull/642#pullrequestreview-4800606997
claude	2026-07-28T18:24:02Z	https://github.com/thomasluizon/orbit-ui-mobile/pull/642#pullrequestreview-4800606997
exit 0

Acceptance evidence

The new static case was calibrated against the original invocation before the fix:

PASS merge sweep CLI flag guard scans both real script filenames
FAIL merge-sweep.sh: never combines --slurp with --jq or --template
FAIL merge-sweep-cov.sh: never combines --slurp with --jq or --template

The same case passes after the fix:

PASS merge sweep CLI flag guard scans both real script filenames
PASS merge-sweep.sh: never combines --slurp with --jq or --template
PASS merge-sweep-cov.sh: never combines --slurp with --jq or --template

The existing decision paths prove the required behavior remains reachable in both twins:

PASS merge-sweep.sh: matching expected head and clean review lookups merge
PASS merge-sweep.sh: a newer review skips without merging
PASS merge-sweep.sh: reviews lookup failure fails closed by name
PASS merge-sweep-cov.sh: matching expected head and clean review lookups merge
PASS merge-sweep-cov.sh: a newer review skips without merging
PASS merge-sweep-cov.sh: reviews lookup failure fails closed by name
HARNESS LOCKSTEP OK: 6 pairs checked
MERGE-SWEEP REVIEW LOOKUP TWINS OK

UNMET: The ticket requests a real end-to-end merge of a qualifying pull request. The standing worker contract forbids merging any pull request and forbids touching another ticket. No real pull request was merged. The hermetic harness reaches the merge decision and asserts the merge call for both scripts.

Gates

npm run lint

Tasks:    3 successful, 3 total
Cached:    3 cached, 3 total
Time:    345ms
exit 0

npm run type-check

Tasks:    3 successful, 3 total
Cached:    3 cached, 3 total
Time:    325ms
exit 0

npm run test

Tasks:    4 successful, 4 total
Cached:    4 cached, 4 total
Time:    591ms
exit 0

node tools/check-lockstep.mjs

HARNESS LOCKSTEP OK: 6 pairs checked
exit 0

node tools/test-tools.mjs

# structural coverage
PASS every tools/ script has coverage (28 scripts)
PASS the coverage guard actually enumerated scripts

# universal contract (tools/CONVENTIONS.md)
PASS a real bash is resolvable
PASS agent-review.ps1: --help exits 0 with usage on stdout
PASS agent-review.ps1: invalid input is refused
PASS agent-review.sh: --help exits 0 with usage on stdout
PASS agent-review.sh: invalid input is refused
PASS arch-map.mjs: --help exits 0 with usage on stdout
PASS arch-map.mjs: invalid input is refused
PASS capture-surfaces.mjs: --help exits 0 with usage on stdout
PASS capture-surfaces.mjs: invalid input is refused
PASS check-context-budget.mjs: --help exits 0 with usage on stdout
PASS check-context-budget.mjs: invalid input is refused
PASS check-copy.mjs: --help exits 0 with usage on stdout
PASS check-copy.mjs: invalid input is refused
PASS check-dashes.mjs: --help exits 0 with usage on stdout
PASS check-dashes.mjs: invalid input is refused
PASS check-frontmatter.mjs: --help exits 0 with usage on stdout
PASS check-frontmatter.mjs: invalid input is refused
PASS check-lockstep.mjs: --help exits 0 with usage on stdout
PASS check-lockstep.mjs: invalid input is refused
PASS check-push-target.mjs: --help exits 0 with usage on stdout
PASS check-push-target.mjs: invalid input is refused
PASS check-suppressions-ratchet.mjs: --help exits 0 with usage on stdout
PASS check-suppressions-ratchet.mjs: invalid input is refused
PASS check-ticket.mjs: --help exits 0 with usage on stdout
PASS check-ticket.mjs: invalid input is refused
PASS compose-prompt.mjs: --help exits 0 with usage on stdout
PASS compose-prompt.mjs: invalid input is refused
PASS launch-worker.mjs: --help exits 0 with usage on stdout
PASS launch-worker.mjs: invalid input is refused
PASS merge-sweep-cov.sh: --help exits 0 with usage on stdout
PASS merge-sweep-cov.sh: invalid input is refused
PASS merge-sweep.sh: --help exits 0 with usage on stdout
PASS merge-sweep.sh: invalid input is refused
PASS new-ticket.mjs: --help exits 0 with usage on stdout
PASS new-ticket.mjs: invalid input is refused
PASS nudge-worker.mjs: --help exits 0 with usage on stdout
PASS nudge-worker.mjs: invalid input is refused
PASS orca-web-port.mjs: --help exits 0 with usage on stdout
PASS orca-web-port.mjs: invalid input is refused
PASS pr-watch.mjs: --help exits 0 with usage on stdout
PASS pr-watch.mjs: invalid input is refused
PASS preflight.mjs: --help exits 0 with usage on stdout
PASS preflight.mjs: invalid input is refused
PASS redesign-coverage.mjs: --help exits 0 with usage on stdout
PASS redesign-coverage.mjs: invalid input is refused
PASS rollup.sh: --help exits 0 with usage on stdout
PASS rollup.sh: invalid input is refused
PASS surface-manifest.mjs: --help exits 0 with usage on stdout
PASS surface-manifest.mjs: invalid input is refused
PASS teardown-worktree.mjs: --help exits 0 with usage on stdout
PASS teardown-worktree.mjs: invalid input is refused
PASS wave-plan.mjs: --help exits 0 with usage on stdout
PASS wave-plan.mjs: invalid input is refused
PASS worker-status.mjs: --help exits 0 with usage on stdout
PASS worker-status.mjs: invalid input is refused
PASS worker-watch.mjs: --help exits 0 with usage on stdout
PASS worker-watch.mjs: invalid input is refused

# decision paths
PASS merge sweep CLI flag guard scans both real script filenames
PASS merge-sweep.sh: never combines --slurp with --jq or --template
PASS merge-sweep-cov.sh: never combines --slurp with --jq or --template
PASS merge-sweep.sh: matching expected head and clean review lookups merge
PASS merge-sweep.sh: changed head skips and names both SHAs
PASS merge-sweep.sh: atomic merge refusal reports a last-moment head change
PASS merge-sweep.sh: invocation without expected head still merges
PASS merge-sweep.sh: help documents the exclusive cutoff and residual post-merge window
PASS merge-sweep.sh: a routine update adopts and rechecks the new head before merging
PASS merge-sweep.sh: an adversarial update without the expected parents is rejected
PASS merge-sweep.sh: a failing check on the adopted head skips without merging
PASS merge-sweep.sh: an unsettled current-head review check skips without merging
PASS merge-sweep.sh: a reconciled reply before the refreshed cutoff merges
PASS merge-sweep.sh: activity in the residual merge window is reported after the merge
PASS merge-sweep.sh: unresolved threads in the residual merge window are reported after the merge
PASS merge-sweep.sh: a review lookup failure after the merge is reported by source
PASS merge-sweep.sh: a post-merge review failure stops the multi-PR sweep
PASS merge-sweep.sh: genuine third-party activity at the refreshed cutoff skips
PASS merge-sweep.sh: unresolved review threads skip without merging
PASS merge-sweep.sh: a newer review skips without merging
PASS merge-sweep.sh: an already-submitted COMMENTED review edited after the cutoff skips without merging
PASS merge-sweep.sh: pagination sees a newer review timestamp on page two
PASS merge-sweep.sh: a newer issue comment skips without merging
PASS merge-sweep.sh: review-thread lookup failure fails closed by name
PASS merge-sweep.sh: reviews lookup failure fails closed by name
PASS merge-sweep.sh: issue-comments lookup failure fails closed by name
PASS merge-sweep.sh: a COMMENTED review edited strictly before the cutoff still merges
PASS merge-sweep.sh: a newer inline comment on a resolved thread skips without merging
PASS merge-sweep.sh: an older inline comment still merges
PASS merge-sweep.sh: an inline comment edited after the cutoff skips by updated time
PASS merge-sweep.sh: inline-comment lookup failure fails closed by name
PASS merge-sweep.sh: pagination sees a newer inline comment on page two
PASS merge-sweep.sh: reviews exactly at reviewed-through skip without merging
PASS merge-sweep.sh: reviews strictly after reviewed-through skip without merging
PASS merge-sweep.sh: reviews strictly before reviewed-through still merge
PASS merge-sweep.sh: inline comments exactly at reviewed-through skip without merging
PASS merge-sweep.sh: inline comments strictly after reviewed-through skip without merging
PASS merge-sweep.sh: inline comments strictly before reviewed-through still merge
PASS merge-sweep.sh: conversation comments exactly at reviewed-through skip without merging
PASS merge-sweep.sh: conversation comments strictly after reviewed-through skip without merging
PASS merge-sweep.sh: conversation comments strictly before reviewed-through still merge
PASS merge-sweep.sh: a missing reviewed-through mapping fails closed
PASS merge-sweep-cov.sh: matching expected head and clean review lookups merge
PASS merge-sweep-cov.sh: changed head skips and names both SHAs
PASS merge-sweep-cov.sh: atomic merge refusal reports a last-moment head change
PASS merge-sweep-cov.sh: invocation without expected head still merges
PASS merge-sweep-cov.sh: help documents the exclusive cutoff and residual post-merge window
PASS merge-sweep-cov.sh: a routine update adopts and rechecks the new head before merging
PASS merge-sweep-cov.sh: an adversarial update without the expected parents is rejected
PASS merge-sweep-cov.sh: a failing check on the adopted head skips without merging
PASS merge-sweep-cov.sh: an unsettled current-head review check skips without merging
PASS merge-sweep-cov.sh: a reconciled reply before the refreshed cutoff merges
PASS merge-sweep-cov.sh: activity in the residual merge window is reported after the merge
PASS merge-sweep-cov.sh: unresolved threads in the residual merge window are reported after the merge
PASS merge-sweep-cov.sh: a review lookup failure after the merge is reported by source
PASS merge-sweep-cov.sh: a post-merge review failure stops the multi-PR sweep
PASS merge-sweep-cov.sh: genuine third-party activity at the refreshed cutoff skips
PASS merge-sweep-cov.sh: unresolved review threads skip without merging
PASS merge-sweep-cov.sh: a newer review skips without merging
PASS merge-sweep-cov.sh: an already-submitted COMMENTED review edited after the cutoff skips without merging
PASS merge-sweep-cov.sh: pagination sees a newer review timestamp on page two
PASS merge-sweep-cov.sh: a newer issue comment skips without merging
PASS merge-sweep-cov.sh: review-thread lookup failure fails closed by name
PASS merge-sweep-cov.sh: reviews lookup failure fails closed by name
PASS merge-sweep-cov.sh: issue-comments lookup failure fails closed by name
PASS merge-sweep-cov.sh: a COMMENTED review edited strictly before the cutoff still merges
PASS merge-sweep-cov.sh: a newer inline comment on a resolved thread skips without merging
PASS merge-sweep-cov.sh: an older inline comment still merges
PASS merge-sweep-cov.sh: an inline comment edited after the cutoff skips by updated time
PASS merge-sweep-cov.sh: inline-comment lookup failure fails closed by name
PASS merge-sweep-cov.sh: pagination sees a newer inline comment on page two
PASS merge-sweep-cov.sh: reviews exactly at reviewed-through skip without merging
PASS merge-sweep-cov.sh: reviews strictly after reviewed-through skip without merging
PASS merge-sweep-cov.sh: reviews strictly before reviewed-through still merge
PASS merge-sweep-cov.sh: inline comments exactly at reviewed-through skip without merging
PASS merge-sweep-cov.sh: inline comments strictly after reviewed-through skip without merging
PASS merge-sweep-cov.sh: inline comments strictly before reviewed-through still merge
PASS merge-sweep-cov.sh: conversation comments exactly at reviewed-through skip without merging
PASS merge-sweep-cov.sh: conversation comments strictly after reviewed-through skip without merging
PASS merge-sweep-cov.sh: conversation comments strictly before reviewed-through still merge
PASS merge-sweep-cov.sh: a missing reviewed-through mapping fails closed
PASS new-ticket.mjs: validates the identifier orca reported
PASS new-ticket.mjs: a created but defective ticket exits 1 naming it
PASS new-ticket.mjs: an orca failure creates nothing and exits 3
PASS new-ticket.mjs: success with no identifier is a tool error, never a silent pass
PASS new-ticket.mjs: requires --project so the ticket cannot be orphaned
PASS launch-worker.mjs: Claude defaults to opus
PASS launch-worker.mjs: tier:cheap selects sonnet on Claude
PASS launch-worker.mjs: tier:deep selects a distinct max-effort opus invocation on Claude
PASS launch-worker.mjs: Claude cheap tier cannot resolve to the unchanged default invocation
PASS launch-worker.mjs: Claude deep tier cannot resolve to the unchanged default invocation
PASS launch-worker.mjs: an unknown tier lists the engine's declared cheap and deep tiers
PASS launch-worker.mjs: a codex-only or unknown tier is rejected on Claude
PASS launch-worker.mjs: rejects the legacy worker:sonnet label with tier:cheap remediation
PASS launch-worker.mjs: rejects conflicting tier labels
PASS launch-worker.mjs: resolves the repo from the repo:* label
PASS launch-worker.mjs: derives the contract branch from the title
PASS launch-worker.mjs: refuses a repo:* label with no repos entry
PASS launch-worker.mjs: refuses a ticket with no repo:* label and no --repo
PASS launch-worker.mjs: refuses a prompt file inside a repo
PASS launch-worker.mjs: refuses an engine with no models map
PASS launch-worker.mjs: refuses an engine model map with no default
PASS launch-worker.mjs: refuses a model flag in the engine's base args
PASS launch-worker.mjs: refuses an engine model map with no cheap tier
PASS launch-worker.mjs: refuses an engine model map with no deep tier
PASS launch-worker.mjs: refuses identical cheap and deep mappings
PASS launch-worker.mjs: refuses a selected non-default tier identical to the default
PASS launch-worker.mjs: refuses an engine declaring interactive: false
PASS launch-worker.mjs: refuses an engine that omits interactive entirely
PASS launch-worker.mjs: refuses headless args behind an interactive declaration
PASS launch-worker.mjs: refuses a headless token hidden in the command field
PASS launch-worker.mjs: refuses a claude permission mode that cannot run unattended shell commands
PASS launch-worker.mjs: accepts the required claude permission mode from the whole resolved invocation
PASS launch-worker.mjs: Codex defaults to Sol at high effort
PASS launch-worker.mjs: tier:cheap selects Luna at low effort on Codex
PASS launch-worker.mjs: tier:deep selects Sol at max effort on Codex
PASS launch-worker.mjs: Codex cheap tier cannot resolve to the unchanged default invocation
PASS launch-worker.mjs: Codex deep tier cannot resolve to the unchanged default invocation
PASS launch-worker.mjs: the codex plan's command carries no headless token
PASS launch-worker.mjs: accepts codex -p, which is --profile and not --print
PASS launch-worker.mjs: still refuses codex exec behind an interactive declaration
PASS launch-worker.mjs: refuses codex e, the documented alias for exec
PASS launch-worker.mjs: refuses an engine binary with no profile rather than waving it through
PASS launch-worker.mjs: refuses a missing prompt file
PASS launch-worker.mjs: refuses a non-Linear issue identifier
PASS launch-worker.mjs: injects the standing worker contract into a prompt that lacks it
PASS launch-worker.mjs: does not stack a second copy on relaunch
PASS launch-worker.mjs: answers claude's trust screen on the created-or-trust wording, from the terminal text alone
PASS launch-worker.mjs: answers claude's trust screen on the trust-the-files wording, from the terminal text alone
PASS launch-worker.mjs: answers claude's trust screen on the trust-this-folder wording, from the terminal text alone
PASS launch-worker.mjs: answers codex's trust screen on the trust-the-contents wording, from the terminal text alone
PASS launch-worker.mjs: a pointer that lands on the first send exits 0 with the plan unchanged
PASS launch-worker.mjs: a pointer that lands is sent exactly once
PASS launch-worker.mjs: a pointer the composer swallowed is re-sent, and the plan reports how many sends it took
PASS launch-worker.mjs: a pointer that never becomes a user turn is a launch FAILURE, not a success
PASS launch-worker.mjs: the undelivered launch is bounded, not retried forever
PASS launch-worker.mjs: a TUI that keeps painting is never sent to a second time
PASS launch-worker.mjs: a TUI that never goes quiet is a launch failure naming that cause
PASS launch-worker.mjs: the undelivered launch leaves no orphaned worktree
PASS launch-worker.mjs: a terminal create timeout retries inside the same worktree and then succeeds
PASS launch-worker.mjs: terminal create timeout retries are bounded and preserve the timeout cause
PASS launch-worker.mjs: exhausting terminal create retries rolls back the one worktree and both branches
PASS launch-worker.mjs: the injected contract still forbids asking a question
PASS launch-worker.mjs: the injected contract still forbids dropping a blocked criterion
PASS launch-worker.mjs: the injected contract still forbids watching its own PR or another ticket
PASS launch-worker.mjs: the injected contract still forbids arming a monitor that outlives the contract
PASS launch-worker.mjs: the injected contract still forbids resolving a watch-and-stop conflict by doing both
PASS launch-worker.mjs: the injected contract still forbids merging or pushing to main
PASS launch-worker.mjs: the injected contract still forbids blanket staging that sweeps in a sibling's artifacts
PASS launch-worker.mjs: the injected contract still forbids pushing a commit it has not read back
PASS launch-worker.mjs: the injected contract still forbids writing into another worker's worktree
PASS launch-worker.mjs: the injected contract still forbids delegating independent slices while keeping conflicts and PR evidence inline
PASS preflight.mjs: a clean base-branch environment prints an all-PASS table
PASS preflight.mjs: the known-good Claude policy is accepted from the whole resolved invocation
PASS preflight.mjs: a Claude acceptEdits invocation fails with the known-good remedy
PASS preflight.mjs: an unauthenticated GitHub CLI fails and names the login remedy
PASS preflight.mjs: an unreachable Orca runtime fails and names the restart remedy
PASS preflight.mjs: a dirty target working tree fails and names the cleanup remedy
PASS preflight.mjs: a target repo off its base branch fails and names both branches
PASS preflight.mjs: a missing ticket-specific CLI fails and names its install remedy
PASS preflight.mjs: machine-readable output carries stable check ids and the verdict
PASS nudge-worker.mjs: --help documents the engine override and fail-closed rule
PASS nudge-worker.mjs: rejects multi-line text
PASS nudge-worker.mjs: rejects --text together with --prompt-file
PASS nudge-worker.mjs: rejects a non-positive --wait-attempts
PASS nudge-worker.mjs: refuses to send while the worker is busy
PASS nudge-worker.mjs: an orca failure that is not a timeout is a tool error
PASS nudge-worker.mjs: sends once both signals say the worker is idle
PASS nudge-worker.mjs: sends once both signals say the worker is idle sends 1 time(s)
PASS nudge-worker.mjs: trusts the codex ready composer structure with › Explain this codebase
PASS nudge-worker.mjs: trusts the codex ready composer structure with › Explain this codebase sends 1 time(s)
PASS nudge-worker.mjs: trusts the codex ready composer structure with › Run /review on my current changes
PASS nudge-worker.mjs: trusts the codex ready composer structure with › Run /review on my current changes sends 1 time(s)
PASS nudge-worker.mjs: trusts the codex ready composer structure with › Write tests for @filename
PASS nudge-worker.mjs: trusts the codex ready composer structure with › Write tests for @filename sends 1 time(s)
PASS nudge-worker.mjs: trusts the codex ready composer structure with › Use /skills to list available skills
PASS nudge-worker.mjs: trusts the codex ready composer structure with › Use /skills to list available skills sends 1 time(s)
PASS nudge-worker.mjs: refuses a retained composer marker followed by a static permission screen
PASS nudge-worker.mjs: refuses a retained composer marker followed by a static permission screen sends 0 time(s)
PASS nudge-worker.mjs: refuses a retained composer and status followed by a static permission screen
PASS nudge-worker.mjs: refuses a retained composer and status followed by a static permission screen sends 0 time(s)
PASS nudge-worker.mjs: trusts the measured idle codex tail despite a historical working indicator
PASS nudge-worker.mjs: trusts the measured idle codex tail despite a historical working indicator sends 1 time(s)
PASS nudge-worker.mjs: refuses the measured codex tail with a live working indicator after the composer
PASS nudge-worker.mjs: refuses the measured codex tail with a live working indicator after the composer sends 0 time(s)
PASS nudge-worker.mjs: recognizes structural status with a different codex model and effort
PASS nudge-worker.mjs: recognizes structural status with a different codex model and effort sends 1 time(s)
PASS nudge-worker.mjs: recognizes the first live idle composer shape
PASS nudge-worker.mjs: recognizes the first live idle composer shape sends 1 time(s)
PASS nudge-worker.mjs: refuses the live busy composer shape
PASS nudge-worker.mjs: refuses the live busy composer shape sends 0 time(s)
PASS nudge-worker.mjs: recognizes the second live idle composer shape
PASS nudge-worker.mjs: recognizes the second live idle composer shape sends 1 time(s)
PASS nudge-worker.mjs: ignores answered trust text before the current codex composer
PASS nudge-worker.mjs: ignores answered trust text before the current codex composer sends 1 time(s)
PASS nudge-worker.mjs: refuses a live trust prompt after the current codex composer
PASS nudge-worker.mjs: refuses a live trust prompt after the current codex composer sends 0 time(s)
PASS nudge-worker.mjs: fails closed when a trust prompt has no current composer region
PASS nudge-worker.mjs: fails closed when a trust prompt has no current composer region sends 0 time(s)
PASS nudge-worker.mjs: does not let incidental greater-than output select the claude profile for a codex worker
PASS nudge-worker.mjs: does not let incidental greater-than output select the claude profile for a codex worker sends 0 time(s)
PASS nudge-worker.mjs: fails closed for the explicitly selected unverified claude profile
PASS nudge-worker.mjs: fails closed for the explicitly selected unverified claude profile sends 0 time(s)
PASS nudge-worker.mjs: fails closed when the orchestrator worker is auto
PASS nudge-worker.mjs: fails closed when the orchestrator worker is auto sends 0 time(s)
PASS nudge-worker.mjs: fails closed when the orchestrator worker is unknown
PASS nudge-worker.mjs: fails closed when the orchestrator worker is unknown sends 0 time(s)
PASS nudge-worker.mjs: fails closed when the engine override is unknown
PASS nudge-worker.mjs: fails closed when the engine override is unknown sends 0 time(s)
PASS nudge-worker.mjs: fails closed when the orchestrator worker is missing
PASS nudge-worker.mjs: fails closed when the orchestrator worker is missing sends 0 time(s)
PASS nudge-worker.mjs: fails closed for the configured unverified claude profile
PASS nudge-worker.mjs: fails closed for the configured unverified claude profile sends 0 time(s)
PASS nudge-worker.mjs: --engine overrides a disagreeing orchestrator worker
PASS nudge-worker.mjs: --engine overrides a disagreeing orchestrator worker sends 1 time(s)
PASS nudge-worker.mjs: settles before retrying a trust prompt that remains on screen
PASS nudge-worker.mjs: settles before retrying a trust prompt that remains on screen sends 0 time(s)
PASS nudge-worker.mjs: trust prompt retry applies one settle pause
PASS nudge-worker.mjs: refuses a ready-looking codex composer carrying esc to interrupt
PASS nudge-worker.mjs: refuses a ready-looking codex composer carrying esc to interrupt sends 0 time(s)
PASS nudge-worker.mjs: refuses a ready-looking codex composer carrying esc to interupt
PASS nudge-worker.mjs: refuses a ready-looking codex composer carrying esc to interupt sends 0 time(s)
PASS nudge-worker.mjs: refuses a static trust prompt that is still on screen
PASS nudge-worker.mjs: refuses a static trust prompt that is still on screen sends 0 time(s)
PASS nudge-worker.mjs: refuses an unrecognized static screen with no ready composer signal
PASS nudge-worker.mjs: refuses an unrecognized static screen with no ready composer signal sends 0 time(s)
PASS nudge-worker.mjs: refuses a tui-idle that is still repainting, which is a worker mid-turn
PASS nudge-worker.mjs: refuses a tui-idle that is still repainting, which is a worker mid-turn sends 0 time(s)
PASS nudge-worker.mjs: refuses when both signals say the worker is busy
PASS nudge-worker.mjs: refuses when both signals say the worker is busy sends 0 time(s)
PASS nudge-worker.mjs: does not treat codex-interactive-prompt as the measured stale reason
PASS nudge-worker.mjs: does not treat codex-interactive-prompt as the measured stale reason sends 0 time(s)
PASS nudge-worker.mjs: --dry-run calls orca not at all
PASS pr-watch.mjs: a verdict sitting on an OLDER commit does not satisfy the watch
PASS pr-watch.mjs: a fresh CHANGES_REQUESTED on the current head fires, which is the silent-spin regression
PASS pr-watch.mjs: a fresh approval fires
PASS pr-watch.mjs: an approval the caller already acted on reports mergeable-and-approved instead of repeating itself
PASS pr-watch.mjs: a failing check beats an approval
PASS pr-watch.mjs: a merged PR ends the watch
PASS pr-watch.mjs: watching several PRs reports whichever one transitioned
PASS pr-watch.mjs: the polling loop sleeps, re-polls and times out reporting it, without --once
PASS pr-watch.mjs: the timed-out watch really polled more than once
PASS pr-watch.mjs: refuses a baseline for a PR it is not watching
PASS pr-watch.mjs: refuses a malformed baseline rather than ignoring it
PASS pr-watch.mjs: refuses a repo that is not an owner\/name slug
PASS worker-watch.mjs: an empty fleet says so rather than printing nothing
PASS worker-watch.mjs: a repainting terminal is BUSY, and its repaint tail yields no output lines
PASS worker-watch.mjs: the repaint tail is stripped to nothing rather than printed raw
PASS worker-watch.mjs: the ticket's Linear state is reported alongside liveness
PASS worker-watch.mjs: the rendered terminal handle is complete and directly reusable
PASS worker-watch.mjs: two identical samples are IDLE, and real content survives the stripping
PASS worker-watch.mjs: the last meaningful lines survive, so a worker stopped on a question is readable
PASS worker-watch.mjs: an unreadable contract verdict is reported, never silently dropped
PASS worker-watch.mjs: refuses a repo outside orchestrator.json
PASS worker-watch.mjs: refuses a non-positive --lines
PASS teardown-worktree.mjs: refuses no selector
PASS teardown-worktree.mjs: refuses both selectors
PASS teardown-worktree.mjs: refuses a malformed Linear issue selector
PASS teardown-worktree.mjs: refuses a valueless issue selector
PASS teardown-worktree.mjs: refuses a valueless worktree selector
PASS teardown-worktree.mjs: refuses a valueless base
PASS teardown-worktree.mjs: refuses an issue with no active worktree
PASS teardown-worktree.mjs: refuses a primary checkout
PASS teardown-worktree.mjs: refuses a worktree without a linked Linear issue
PASS teardown-worktree.mjs: runtime_unavailable is success when filesystem and git verification prove removal
PASS teardown-worktree.mjs: verified removal actually deleted the fixture
PASS teardown-worktree.mjs: ignores another fleet terminal without a worktree path
PASS teardown-worktree.mjs: a dirty tree is refused with its uncommitted path
PASS teardown-worktree.mjs: dirty refusal leaves the tree untouched
PASS teardown-worktree.mjs: content absent from the target branch is refused
PASS teardown-worktree.mjs: a closed-looking but non-Done Linear issue is refused
PASS teardown-worktree.mjs: a repainting terminal is refused because the worker is still working
PASS teardown-worktree.mjs: an ok removal response is failure when the directory survives
PASS teardown-worktree.mjs: a path selector accepts a squash-merged tree without ancestry
PASS teardown-worktree.mjs: a squash-merged tree is present when the target advanced on unrelated paths
PASS teardown-worktree.mjs: reports a branch deletion failure after removing the worktree
PASS teardown-worktree.mjs: reports a branch that remains after deletion
PASS orca-web-port.mjs: rejects multiple operation flags
PASS orca-web-port.mjs: rejects --name without --derive
PASS orca-web-port.mjs: requires a name for --derive
PASS orca-web-port.mjs: derives the same port for the same name
PASS orca-web-port.mjs: keeps generated ports inside the guarded web window
PASS orca-web-port.mjs: a linked worktree without setup refuses to guess
PASS orca-web-port.mjs: setup assigns the first linked worktree
PASS orca-web-port.mjs: setup assigns a different linked worktree
PASS orca-web-port.mjs: linked worktrees report their own distinct assignments
PASS orca-web-port.mjs: setup does not clobber an existing local environment file
PASS orca-web-port.mjs: refuses a deterministic port collision before persisting
PASS orca-web-port.mjs: collision refusal leaves no marker behind
PASS orca-web-port.mjs: the primary checkout keeps the default port
PASS orca-web-port.mjs: setup refuses the primary checkout
PASS worker-status.mjs: requires --worktree
PASS worker-status.mjs: requires a Linear issue identifier
PASS worker-status.mjs: screenshot and critique present is OK
PASS worker-status.mjs: extensionless Linear upload and separate critique is OK
PASS worker-status.mjs: extensionless Linear upload alone is not a critique
PASS worker-status.mjs: screenshot present and critique missing is UNMET
PASS worker-status.mjs: neither screenshot nor critique present is UNMET
PASS compose-prompt.mjs: writes the body and chronological, attributed comments without changing fenced Markdown
PASS compose-prompt.mjs: comment order, attribution, and fences survive composition
PASS launch-worker.mjs: accepts a composed prompt file unchanged
PASS compose-prompt.mjs: omits the comments heading when the issue has no comments
PASS compose-prompt.mjs: zero comments add no empty heading
PASS wave-plan.mjs: documents the explicit issue selection mode
PASS wave-plan.mjs: plans one explicitly requested identifier and counts out-of-set dependents in reach
PASS wave-plan.mjs: deduplicates explicitly requested identifiers before fetching
PASS wave-plan.mjs: renders both members of an explicit two-ticket selection
PASS wave-plan.mjs: refuses explicit issues combined with another mode
PASS wave-plan.mjs: refuses explicit issues combined with a label
PASS wave-plan.mjs: refuses explicit issues combined with all
PASS wave-plan.mjs: requires a value for explicit issues
PASS wave-plan.mjs: names an unresolved requested identifier
PASS wave-plan.mjs: refuses a requested Done identifier
PASS wave-plan.mjs: uses an out-of-set team blocker while displaying only requested issues
PASS wave-plan.mjs: restricts text output to requested identifiers with their blocker state
PASS wave-plan.mjs: orders a blockedBy pair into two waves
PASS wave-plan.mjs: wave 1 is the unblocked ticket
PASS wave-plan.mjs: reach counts the whole downstream chain, not just direct blockers
PASS wave-plan.mjs: a wave-1 ticket at the strike limit is reported, not dropped
PASS wave-plan.mjs: text mode marks the same strike-limit ticket
PASS wave-plan.mjs: an empty project is nothing to plan
PASS wave-plan.mjs: fetches 100 relations in a bounded pool while preserving the table order
PASS wave-plan.mjs: names a failing relation fetch without an execFile stack trace
PASS wave-plan.mjs: keeps planning when one external blocker cannot be fetched
PASS check-dashes.mjs: an em dash in text is rejected
PASS check-dashes.mjs: clean text passes
PASS check-lockstep.mjs: six matching pairs pass
PASS check-lockstep.mjs: a malformed declaration manifest fails loudly
PASS check-lockstep.mjs: uses orchestrator repos.api when --api-root is omitted
PASS check-lockstep.mjs: falls back to the sibling when orchestrator repos.api is missing
PASS check-lockstep.mjs: falls back to the sibling when orchestrator config is unparsable
PASS check-lockstep.mjs: falls back to the sibling when orchestrator config is absent
PASS check-lockstep.mjs: an undeclared divergence fails with its file and region
PASS check-lockstep.mjs: a declaration that matches no current diff is stale
PASS check-lockstep.mjs: a justified declared divergence passes
PASS check-lockstep.mjs: a change in the shared region still fails
PASS check-lockstep.mjs: second-opinion drift fails byte for byte
PASS check-lockstep.mjs: an unreachable sibling fails loudly
PASS check-context-budget.mjs: total over baseline exits 1 and names the offending file
PASS check-context-budget.mjs: total under baseline exits 0
PASS check-context-budget.mjs: an under-budget check does not rewrite context-budget.json
PASS check-context-budget.mjs: a grown branch can regenerate its working baseline
PASS check-context-budget.mjs: a regenerated working baseline cannot hide growth from the target branch
PASS check-context-budget.mjs: a first-run baseline bootstraps only when absent from the target branch
PASS check-context-budget.mjs: an unfetched target branch fails closed
PASS check-context-budget.mjs: a removed sibling import fails even when its target is absent
PASS check-context-budget.mjs: a new unconditional rules file exits 1
PASS check-context-budget.mjs: a rules file with paths frontmatter stays outside the budget
PASS check-context-budget.mjs: missing sibling repos do not fail the check
PASS check-context-budget.mjs: missing sibling files are omitted from the printed full-session table
PASS check-context-budget.mjs: present sibling files without imports do not change the enforced verdict
PASS check-context-budget.mjs: present sibling files without imports stay outside the printed full-session table
PASS check-context-budget.mjs: a malformed baseline is a tool error
PASS check-context-budget.mjs: help names every flag and every exit code
PASS capture-surfaces.mjs: uses the primary checkout default when no base URL is supplied
PASS capture-surfaces.mjs: uses the linked worktree port when no base URL is supplied
PASS capture-surfaces.mjs: keeps an explicit base URL over the assigned port
PASS capture-surfaces.mjs: refuses capture when a linked worktree has no assigned port
PASS check-ticket.mjs: an incomplete body is rejected
PASS check-ticket.mjs: a missing body file is a usage error
PASS check-ticket.mjs: an acceptance criterion quantifying over an open set is rejected
PASS check-ticket.mjs: the same criterion passes once it names the command that decides it
PASS check-ticket.mjs: a bound outside the quantified clause does not rescue an open set
PASS check-ticket.mjs: an acceptance criterion trailing off into an unnamed remainder is rejected
PASS check-ticket.mjs: a visible-effect body with screenshots and critique passes
PASS check-ticket.mjs: a visible-effect body with screenshots but no critique names the missing critique
PASS check-ticket.mjs: a visible-effect body with neither screenshots nor critique fails both requirements
PASS check-ticket.mjs: issue mode rejects zero type labels and names every valid value
PASS check-ticket.mjs: issue mode accepts exactly one type label
PASS check-ticket.mjs: issue mode rejects two type labels
PASS check-ticket.mjs: the repo label rule still rejects two repo labels alongside one type
PASS check-ticket.mjs: file mode remains unaffected by issue-only label validation
PASS check-ticket.mjs: dependency prose ignores once used as a measured frequency
PASS check-ticket.mjs: dependency prose ignores depends on used for ordinary logic
PASS check-ticket.mjs: dependency prose ignores after used as an ordinary sequence
PASS check-ticket.mjs: dependency prose ignores after used for process order
PASS check-ticket.mjs: dependency prose ignores once used for retry timing
PASS check-ticket.mjs: dependency prose ignores depends on and blocked by used for ordinary behavior
PASS check-ticket.mjs: a genuine named dependency without a relation is rejected
PASS check-ticket.mjs: a named issue blocker still requires a blockedBy relation
PASS check-ticket.mjs: an issue named anywhere in Dependencies requires a blockedBy relation
PASS check-ticket.mjs: a Dependencies section with no issue and no dependency phrase is accepted
PASS check-ticket.mjs: a dependency-free Dependencies section may use ordinary signal words
PASS check-ticket.mjs: a named dependency with its blockedBy relation is accepted
PASS check-push-target.mjs: a push to main is blocked
PASS check-push-target.mjs: a push to a feature branch is allowed
PASS check-frontmatter.mjs: runs from any cwd
PASS check-frontmatter.mjs: accepts a custom root relative to the caller
PASS check-frontmatter.mjs: rejects an unquoted colon-space scalar in a custom root
PASS check-frontmatter.mjs: rejects a missing custom root
PASS check-frontmatter.mjs: rejects a custom root that proves nothing

ORBIT TOOLS GATE OK

Decisions taken unattended

  • Fast-forwarded the clean ticket branch to origin/main before editing because the launched worktree base did not contain the strict sweep implementation named by ORB-141. This exposed the exact defect described by the ticket without introducing unrelated changes.
  • Kept the genuine end-to-end merge criterion explicitly unmet because the standing worker contract prohibits merging and touching another ticket. Used the live API for the affected query and the hermetic sweep harness for merge-decision and skip-path proof.
  • Left all existing fail-closed branches and activity semantics unchanged.

@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 8:08pm

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.

/pr-review — PR #646 (ORB-141)

Scope: the merge-sweep review-activity lookup scripts plus their shared test harness. Changed files:

  • tools/merge-sweep.sh — Repo-tool appeal: file path cited only as diff scope for this review report, not surfaced as an executable command
  • tools/merge-sweep-cov.sh — Repo-tool appeal: file path cited only as diff scope for this review report, not surfaced as an executable command
  • tools/test-tools.mjs

Recommendation: APPROVE

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

Summary

ORB-141 removes the unsupported --slurp flag from the paginated GraphQL review-activity lookup in both merge-sweep twins, since GitHub CLI 2.88.1 rejects --slurp combined with --jq/--template. The --jq selector is updated to read .data.repository.pullRequest.reviews.nodes[] | ... directly off each un-slurped page, which is the correct shape for how gh api graphql --paginate applies --jq per page (vs. once over a slurped array) — consistent with the sibling REST-endpoint lookups' existing .[] | ... pattern in the same file. A new static harness guard (mergeSweepCliFlagCases in tools/test-tools.mjs) scans both real script files and fails if any gh api invocation combines --slurp with --jq/--template; the existing pagination assertion was updated to require --paginate and forbid --slurp. Diff is scoped entirely to tools/** — no apps/*, packages/shared, or orbit-api touched.

Findings

None across all severities.

Subagents

All five gated subagents (parity-checker, i18n-syncer, contract-aligner, security-reviewer, design-reviewer) are N/A — the diff touches no apps/*, packages/shared/src/types/*, orbit-api, or UI files.

Deferred

  • Dimensions 8–14 (DESIGN.md, Parity, i18n, Contract drift, Security, Backend hard rules, FEATURES.md): N/A, surface never touched by this tools-only diff.
  • Phase 6 (/validate) skipped per this workflow's CI adaptation — Build / Unit Tests / SonarCloud run as separate required checks on this PR.
  • Harness Execution evidence for dimension 15 comes from the PR body's own before/after command output (lint/type-check/test/check-lockstep.mjs/test-tools.mjs, including the new guard failing pre-fix and passing post-fix), corroborated by static reading of both changed shell scripts (confirmed no stray --slurp remains and no false-positive risk in the new regex guard).

What's good

Root-cause fix (no fallback/workaround), a genuine regression guard added for the exact defect class, unusually rigorous PR-body evidence (live before/after API calls, calibrated fail→pass harness output), and an honest callout of the one acceptance criterion (a real end-to-end merge) left unmet under the worker contract rather than glossed over.

@thomasluizon

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 060fadd09a

ℹ️ 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".

@thomasluizon

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 9c6946c264

ℹ️ 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".

@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 #646

Scope: PR #646 in thomasluizon/orbit-ui-mobile
Recommendation: APPROVE

Summary

PR #646 (ORB-141) removes the unsupported gh api graphql --slurp flag (incompatible with --jq/--template on GitHub CLI 2.88.1) from the paginated review-activity lookup in both merge-sweep twins (merge-sweep.sh:316, merge-sweep-cov.sh:338), adjusts the jq selector to match the now-unwrapped paginated response shape, and adds a static harness guard (mergeSweepCliFlagCases in test-tools.mjs:2220) that fails if either twin ever recombines --slurp with --jq/--template again. The fix is narrowly scoped, backed by live gh api evidence against a real PR (#642) showing before (exit 1) / after (exit 0, correct TSV rows), and the PR body carries a full pasted test-harness run (571 assertions, ORBIT TOOLS GATE OK) plus green lint / type-check / test and lockstep-check output.

Note: since the prior review on this PR, the branch picked up a merge commit from origin/main (bringing in the already-merged, unrelated ORB-120 calibration-gate commit). Diffing the PR's actual merge base against the current head confirms the PR's own three changed files are unchanged since that review — the merge added no new PR-authored content.

Findings

Critical

None.

High

None.

Medium

None.

Low / Info

[INFO] Acceptance criterion for a real end-to-end merge is honestly disclosed as UNMET

  • dimension: 1 (Correctness)
  • location: orbit-ui-mobile/PR #646 body, "UNMET" section
  • issue: The ticket asked for proof via a real qualifying PR actually merging end-to-end; the author could not do this because the standing worker contract forbids merging a PR or touching another ticket.
  • risk: None — disclosed plainly rather than papered over, and proven two other ways: live gh api evidence against a real, already-merged PR (#642) showing the exact before/after invocation and output, and the hermetic test harness asserting the merge call is reached and made for both twins.
  • fix: No action needed; noted only so the human merging is aware one acceptance criterion was substituted with equivalent evidence.
  • reference: CLAUDE.md D7 (evidence gate) / rubric dimension 1

Subagents

Agent Verdict
parity-checker N/A — diff touches only tooling scripts
i18n-syncer N/A — no user-facing strings / locale JSON changed
contract-aligner N/A — no shared types / orbit-api changed
security-reviewer N/A — orbit-api not touched
design-reviewer N/A — no UI files touched

Validation

Running inside the CI wrapper, so this cites the PR's own CI checks and the executed evidence pasted in the PR body rather than a local re-run.

Check Result
Lint PASS (CI job Lint, SUCCESS)
Type check PASS (CI job Type Check, SUCCESS)
Tests PASS (CI job Unit Tests, SUCCESS; PR body also carries the test-harness run: 571/571 PASS, ORBIT TOOLS GATE OK)
Harness Lockstep PASS (CI job Harness Lockstep, SUCCESS; PR body also carries the lockstep-check output → HARNESS LOCKSTEP OK: 6 pairs checked)
Harness Execution (dimension 15 gate) The PR body's pasted harness output already shows the new "merge sweep CLI flag guard" cases passing for both twins
Build (api) N/A — orbit-api not touched

Deferred — N/A dimensions & files not verdicted

  • Dimensions 8-14 (DESIGN.md, Parity, i18n, Contract drift, Security, Backend hard rules, FEATURES.md): N/A — diff touches only tooling scripts, no apps/*, packages/shared, or orbit-api surface.

All three changed files (merge-sweep-cov.sh, merge-sweep.sh, test-tools.mjs) received a verdict above.

What's good

  • Root-cause fix, not a workaround: the broken flag combination is removed outright.
  • Proven against the real GitHub API (PR #642), not just asserted.
  • A regression guard is added in the same PR so this exact class of bug cannot recur unnoticed.
  • Both merge-sweep twins changed in lockstep, consistent with the harness lockstep contract.
  • Unusually rigorous PR body evidence: live before/after CLI output, the new static case's before/after PASS→PASS flip, and a full harness run pasted verbatim.

Recommendation

Approve. Small, well-evidenced, root-cause bug fix with its own regression coverage added in the same change. No Critical or High findings survive review. This branch's only change since the prior review is a no-op merge from main.

@thomasluizon

Copy link
Copy Markdown
Owner Author

READY-TO-MERGE 9c6946c

@thomasluizon
thomasluizon marked this pull request as ready for review July 28, 2026 20:33
@thomasluizon
thomasluizon merged commit a03f403 into main Jul 28, 2026
31 checks passed
@thomasluizon
thomasluizon deleted the feature/orb-141-the-merge-sweep-can-never-merge branch July 28, 2026 20:33
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