Skip to content

feat(sweep): classify question-card gates and name the unverifiable residual (BLO-30627) - #1541

Merged
allyblockcast[bot] merged 4 commits into
masterfrom
feat/blo-30627-gate-kind-coverage
Aug 29, 2026
Merged

feat(sweep): classify question-card gates and name the unverifiable residual (BLO-30627)#1541
allyblockcast[bot] merged 4 commits into
masterfrom
feat/blo-30627-gate-kind-coverage

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 29, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • The human-gated ageing sweep reports how long each human-gated row has been silent, and BLO-30608 added a pass that re-tests whether each row's gate is still real
  • That pass reads exactly two evidence kinds — blockedBy targets and linked approvals — so any row gated on anything else lands unverifiable and ages exactly as before. BLO-30608's own AC3 said a high unverifiable count "is itself the finding"
  • It was 90.9% of 746 rows: for 678 of them the pass changed nothing, and one opaque bucket that large is a number nobody can act on
  • This pull request adds a third gate kind (question cards), names the residual by why nothing was checkable, and resolves a probe cap that had drifted below the live population
  • The benefit is that 4.4pp of the queue moved out of the opaque bucket into actionable verdicts, and the 86.5% that remains is now five named categories instead of one — including the finding that 456 rows are simply un-started human work that no probe will ever move

Linked Issues or Issue Description

Related PRs found by dedup search (gh pr list --state all, filtered on gate/revalidation/interaction):

No other open or merged PR covers gate-kind coverage in the re-validation classifier.

Checked ROADMAP.md: the only adjacent line is "explicit review and approval stages as first-class workflow steps" (L45). That is about creating approval workflows; this is read-only reporting over gates that already exist. No overlap with planned core work.

What Changed

  • New third probe probePendingInteraction reading issue_thread_interactions — the cards this codebase creates when an agent asks its human a question, confirmation, or checkbox verdict. Pending (or unrecognised-status) → still-gated; at least one human decision → interaction-answered; every card withdrawn/expired/failed → interaction-abandoned.
  • interaction-abandoned is ranked with blocker-cancelled-edge-stuck in a new NON_SELF_CLEARING_RESOLUTION_KINDS list. Neither can self-clear, so both lead the report ahead of merely-finished kinds.
  • New UnverifiableReason splitting the residual five ways, derived from the row's own status so it costs no extra query. Surfaced on the classification, counted in countsByUnverifiableReason, and rendered in the digest.
  • DEFAULT_MAX_PROBES 600 → 2000, with the derivation in the comment rather than as a bare number.
  • GateEvidenceInput gains interactions (required) and status (optional). interactions is required so a loader that forgets to populate it fails to compile — a silently-absent array would make a row with a live question card read as expressing no gate.
  • loadGateEvidence issues a third batched query at the same AGGREGATE_CHUNK_SIZE (500) and threads status through.
  • Backfill script fetches interaction evidence per row (issued concurrently with approvals), reports both new breakdowns, and documents the API-path side effect below.

Verification

Testsnpx vitest run server/src/__tests__/human-gated144 passed, 5 files, 0 failures. 76 of those are the three re-validation suites (64 pure + backfill, 12 wiring/embedded-Postgres). npx tsc --noEmit -p server/tsconfig.json clean.

Mutation-check — each new classifier branch was reverted and the suite re-run:

mutation cases failed
drop probePendingInteraction from PROBES 3
collapse interaction-abandoned into interaction-answered 5
pin classifyUnverifiableReason to status-unreadable 7

Live measurementnpx tsx scripts/blo-30608-gate-revalidation-backfill.ts --source=api --all, 2026-08-29T05:1xZ. Population had grown 746 → 748 since the baseline, so shares are near- but not exactly-comparable:

class baseline now
still-gated 16 (2.1%) 24 (3.2%)
resolved-but-open 52 (7.0%) 77 (10.3%)
unverifiable 678 (90.9%) 647 (86.5%) ▼ 4.4pp

The 647 residual, now named:

why nothing was checkable rows
queued (todo/backlog) — waiting on attention, not a gate 456
in_review but no approval card exists 174
in progress, gated outside this system 17
blocked but no blocker edge exists 0
status unreadable 0

That 456 is the honest answer to why most of the queue is unverifiable: it is un-started human work, not work gated on anything a probe could re-test. New actionable finding the pass could not previously see: 18 rows whose every question card was withdrawn or expired — a human was asked and never replied.

Cost moved 775 → 1525 round trips and 99.0s → 149.8s, all of it the API path's second per-issue call. The DB path gained 2 batched queries.

Two defects the live run caught that the tests now pin: an evidence line that described a mixed row as uniformly answered (observed on BLO-2880, where one card had actually expired), and a wiring-suite teardown that did not clear issue_thread_interactions before issues — the FK failed the delete and every subsequent test inherited the undeleted company, so one missing line read as eight unrelated failures.

Risks

  • One side effect, stated rather than left implicit. On --source=api only, GET /api/issues/:id/interactions first runs expireRequestConfirmationsSupersededByHistoricalComments, so reading it can expire a confirmation card a later comment had already superseded. It touches no issue field and is the endpoint's normal behaviour for any reader including the UI — but "read-only" is an acceptance criterion, so an unstated write would quietly break it. I ran this across 748 live rows to produce the measurement above. The shipping DB path reads the table directly and has no such effect. If a reviewer judges this unacceptable, the fix is to drop interaction evidence from the API path and measure via --source=db; the classifier is unaffected either way. Documented in the script docblock.
  • Two subsystems now read pending interactions with different staleness rules. fix(liveness): expire stale pending interactions from waiting paths #1533 (open) applies a 24-hour boundary in the recovery liveness classifier; this probe treats any pending card as still-gated regardless of age. That is defensible — the ageing sweep already ranks by human silence separately, and this probe answers "is a gate expressed?" not "is it fresh?" — but the two should not silently drift into contradicting each other. Flagged for a reviewer rather than resolved unilaterally.
  • interactions became a required field on GateEvidenceInput. Deliberate, and it is a compile error rather than a runtime surprise; both call sites are updated in this PR. A missing array could never produce a false resolved-but-open (that branch requires a non-empty array), so the failure direction was already safe — required makes it non-reachable.
  • Cap raise is not a cost cliff. The 99s/775-call figure belongs to the API script, not the sweep. loadGateEvidence batches at 500, so the sweep is O(ceil(rows/500)): 9 queries at the measured population, 15 at the new cap.
  • Behaviour change to the weekly digest's rendered output (new sections, new counts). No migration, no schema change, no new table.

Model Used

Claude Opus 4.5 (claude-opus-5[1m] as configured on this agent), 1M context, extended thinking, with tool use and code execution via Claude Code.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — n/a, no UI surface
  • I have updated relevant documentation to reflect my changes (module + script docblocks)
  • I have considered and documented any risks above
  • All Paperclip CI gates are green — not yet; this PR's first run failed on these very template checks
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups — not yet reviewed

🤖 Generated with Claude Code

…esidual (BLO-30627)

BLO-30608 shipped the gate re-validation pass and its AC3 said a high
`unverifiable` count "is itself the finding". It was 90.9% of 746 rows.
This is the follow-up.

Three changes, all still read-only.

1. Third gate kind: `pending-interaction`, reading
   `issue_thread_interactions`. BLO-30608 named permission/RBAC as the
   obvious next probe; it is still the wrong one to build first, for the
   reason its own seam comment gave -- a live access probe needs network
   egress and per-target credentials, and this pass collects inside a
   database transaction. Question cards are where this codebase actually
   expresses "a human has to answer something", they are the largest
   unprobed category, and they are DB-local, so all three of the module's
   stated properties survive. `permission-rbac` stays a declared seam.

   Two resolution kinds, deliberately distinct: `interaction-answered`
   (a human decided) and `interaction-abandoned` (every card withdrawn,
   expired, or failed -- so the human was asked and never replied, and no
   reply is coming). The second is ranked with the stuck cancelled edge
   because neither can self-clear. An unrecognised status counts as live,
   which is property 2 applied to schema drift.

2. `unverifiable` is subdivided by `UnverifiableReason`, derived from the
   row's own status so it costs no extra query. Two are contradictions a
   reader can act on (`blocked` with no blocker edge; `in_review` with no
   approval card); the rest are honest absences.

3. `DEFAULT_MAX_PROBES` 600 -> 2000, with the derivation in the comment.
   The measured 99s/775-call figure belongs to the `--source=api` backfill,
   whose per-issue call is O(rows); the shipping DB path batches at 500, so
   it is O(ceil(rows/500)) -- 9 queries at the measured population, 15 at
   this cap. The cap is a runaway guard, not a cost control, and is set with
   headroom rather than tight against a population that grows.

Measured on the live population, `--source=api --all`, 2026-08-29T05:1xZ
(748 rows; the baseline's was 746):

  still-gated        16 ->  24
  resolved-but-open  52 ->  77
  unverifiable      678 -> 647   90.9% -> 86.5%

  of the 647: 456 queued and waiting on attention rather than a gate,
  174 `in_review` with no approval card, 17 in progress, 0 `blocked`
  without a blocker edge.

New actionable finding the pass could not previously see: 18 rows whose
every question card was withdrawn or expired.

Cost moved 775 -> 1525 round trips and 99.0s -> 149.8s, all of it the
API path's second per-issue call; the DB path gained 2 batched queries.

One side effect is stated rather than left implicit: on `--source=api`
only, `GET /api/issues/:id/interactions` first runs
`expireRequestConfirmationsSupersededByHistoricalComments`, so reading it
can expire a confirmation card a later comment had already superseded.
It touches no issue field and is the endpoint's normal behaviour for any
reader including the UI. The shipping DB path reads the table directly and
has no such effect.

Tests: 144 green across the five human-gated suites (76 in the three
re-validation suites). Each new classifier branch was mutation-checked --
dropping the probe from PROBES, collapsing abandoned into answered, and
pinning the reason to `status-unreadable` fail 3, 5, and 7 cases
respectively. The wiring suite's teardown now clears
`issue_thread_interactions` before `issues`; without it the FK fails the
delete and every subsequent test inherits the undeleted company.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 29, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-30438
🔗 Paperclip issue: BLO-2880
🔗 Paperclip issue: BLO-30608
🔗 Paperclip issue: BLO-30627

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 29, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-30438
🔗 Paperclip issue: BLO-2880
🔗 Paperclip issue: BLO-30608
🔗 Paperclip issue: BLO-30627

@allyblockcast

allyblockcast Bot commented Aug 29, 2026

Copy link
Copy Markdown
Author

@ally please review at head c080f8a — BLO-30627. Three focus areas:

  1. probePendingInteraction (server/src/services/human-gated-gate-revalidation.ts): is the interaction-answered vs interaction-abandoned split right, and is treating an unrecognised status as live the correct fail-safe direction given status is a plain text column?
  2. The stated side effect: GET /api/issues/:id/interactions runs expireRequestConfirmationsSupersededByHistoricalComments, so the --source=api backfill can expire already-superseded confirmation cards. Is documenting it sufficient, or should the API path drop interaction evidence to stay strictly side-effect-free?
  3. DEFAULT_MAX_PROBES 600 -> 2000. The rationale is that the 99s/775-call figure is the API script's cost, not the sweep's (which batches at 500). Does that hold against loadGateEvidence?

@allyblockcast

allyblockcast Bot commented Aug 29, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Verification
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c080f8a

Critical Issues (0)

Important Issues (1)

  • [native-codex] scripts/blo-30608-gate-revalidation-backfill.ts:180 — the DB backfill reports a hard-coded calls: 6, but loadGateEvidence now runs three chunked queries per 500-row batch (blockers, approvals, and interactions) at server/src/services/human-gated-ageing-digest.ts:380, :410, and :434. For the 748-row population cited by this PR, that is 9 evidence queries, plus the candidate query and two clock aggregates, not 6 total. The report therefore understates the measured round-trip cost and makes the documented AC5 output incorrect for any population above 500 (and also overstates it for smaller populations where fewer chunks are used).
    • Derive the count from the actual candidate count and AGGREGATE_CHUNK_SIZE for each query family, or instrument the DB client/query path so the reported value reflects executed round trips. Add coverage for populations spanning 0, 1, 500, and 501 rows.

Suggestions (0)

Strengths

  • The interaction probe fails closed for unknown text statuses and distinguishes human decisions from abandoned cards.
  • Interaction evidence is loaded in bounded database batches and the API path documents its superseding-comment side effect.
  • The new classifier branches have focused pure and wiring tests.

Recommended Action

  1. Fix Important issues before merge.
  2. Address Suggestions opportunistically.

…ing it

`acquireFromDb` reported a hard-coded `calls: 6`. That is the AC5 cost
figure the backfill exists to measure, and a constant cannot be right
across the range: the DB path runs five *batched* query families — the
candidate query's two human-clock aggregates plus `loadGateEvidence`'s
blockers, approvals, and interactions — each once per
`AGGREGATE_CHUNK_SIZE` chunk, so the true cost is
`1 + 5 * ceil(n / chunkSize)`.

The constant was therefore correct only for a single-chunk population.
It understated any population spanning two or more chunks (the live
746-row pass costs 11 round trips, not 6) and overstated an empty one,
where `chunk([])` yields no iterations and the candidate query is the
only trip.

Derives the count from the real population and the real chunk size, and
exports `AGGREGATE_CHUNK_SIZE` so the arithmetic tracks the constant
rather than restating the literal `500`.

The arithmetic is factored into a pure `dbRoundTrips` because
`acquireFromDb` needs a live `DATABASE_URL` and cannot be exercised in
unit tests. Covered across the chunk boundary at 0, 1, 500, 501, the
measured 746, and a non-default chunk size; mutation-checked by
restoring `return 6`, which fails 4 of the 7 new cases.

Found in review of #1541. Read-only is unchanged: this touches only how
the pass reports its own cost.
@allyblockcast

allyblockcast Bot commented Aug 29, 2026

Copy link
Copy Markdown
Author

Addressed in 8853e4f9 — the finding is correct and the constant is now derived.

Confirmed. calls: 6 was hard-coded and it feeds the Cost: N round trips line, i.e. the exact AC5 figure this backfill exists to report. A constant can't be right across the range.

One correction to the arithmetic, which changes the number but not the verdict. The review counts "3 chunked evidence queries + the candidate query + two clock aggregates", treating the clock aggregates as fixed cost. They aren't — latestHumanCommentAt and latestHumanActivityAt are chunked at the same AGGREGATE_CHUNK_SIZE (human-gated-ageing-digest.ts:192, :229). So there are five batched families, not three, and the true cost is:

1 + 5 * ceil(n / AGGREGATE_CHUNK_SIZE)

For the 746-row population that is 11, not the 9 the review derived (3 families × ceil(746/500) = 6 evidence queries, not 9) and not the 6 that shipped.

Both directions the review names are real, and the empty case is the sharper one: chunk([], 500) yields no iterations, so a zero-row population costs 1 round trip while the old constant claimed 6.

What changed:

  • dbRoundTrips(population, chunkSize) — pure, exported, derives the count. acquireFromDb needs a live DATABASE_URL and can't be unit-tested, which is why the arithmetic is factored out rather than instrumented at the DB client. Instrumenting the query path would measure the same number less legibly and would still need a test double.
  • AGGREGATE_CHUNK_SIZE is now exported and passed in, so the reported cost tracks the constant instead of restating 500.
  • Coverage at the boundaries you asked for — 0, 1, 500, 501 — plus the measured 746 and a non-default chunk size, so a change to the chunk size can't silently stop being reflected. Mutation-checked: restoring return 6 fails 4 of the 7 new cases (0, 501, 746, and the chunk-size case).

112 tests green across the four affected suites; tsc --noEmit clean. Read-only is unchanged — this only touches how the pass reports its own cost.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 8853e4f

Prior Findings Dispositioned (1)

  • prior:c080f8a important 1 — fixed — scripts/blo-30608-gate-revalidation-backfill.ts:167dbRoundTrips now derives the DB cost as 1 + 5 * ceil(population / chunkSize), and acquireFromDb passes the candidate count and exported AGGREGATE_CHUNK_SIZE at :210, covering populations across the 500-row chunk boundary.

Critical Issues (0)

Important Issues (1)

  • [native-codex] server/src/services/human-gated-gate-revalidation.ts:713 — the interaction-answered digest heading says “Every question card has been answered,” but probePendingInteraction returns this resolution whenever decided.length > 0, even when the remaining cards are cancelled, expired, or failed (:460-471). Mixed rows are therefore rendered under a heading that contradicts the evidence line (“closed, N by a human decision”) and can lead operators to believe no unanswered card was abandoned.
    • Use wording that covers the actual predicate, such as “At least one question card was answered; remaining cards closed without an answer,” or split the resolution kinds so the heading is truthful. Add a renderer test for one answered plus one expired card.

Suggestions (0)

Strengths

  • The interaction probe fails closed for unknown text statuses, preserving a live-gate interpretation during schema drift.
  • Interaction evidence is loaded in bounded DB batches and the API path documents its superseding-comment side effect.
  • The backfill cost calculation is now derived and tested at empty, single-chunk, and multi-chunk boundaries.

Recommended Action

  1. Fix the Important issue before merge.
  2. Preserve the current exact-head cost derivation and interaction status handling.

`interaction-answered` is assigned whenever *at least one* question card got
a real human decision, so a row with one answered and one expired card lands
under that kind. The digest heading read "Every question card has been
answered", which contradicted the evidence line printed directly beneath it
("closed, 1 by a human decision") and hid the abandoned ask from an operator
scanning headings.

Same class of overstatement the evidence line was already fixed for, one
layer up. Reworded the heading, the `GateResolutionKind` doc, and the
backfill's summary label to match the actual predicate. Added a renderer test
for the mixed case; mutation-checked by restoring the old heading, which
fails it.

Behaviour unchanged: classification, counts and read-only posture are
untouched — this is wording only.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 29, 2026

Copy link
Copy Markdown
Author

Addressed the Important finding in a8e4248.

The finding is correct, and it is the same defect one layer up from one already fixed on this PR. probePendingInteraction assigns interaction-answered whenever decided.length > 0, so a row with one answered and one expired card lands there. The evidence line was already reworded for exactly that case at c080f8a1 — "closed, N by a human decision", pinned by a test that reproduces the live BLO-2880 row. The heading was not, so a mixed row rendered as:

**Every question card has been answered — 1**
- BLO-2880 — [pending-interaction] all 2 thread interactions are closed, 1 by a human decision: i1=expired, i2=answered

The heading and the line beneath it contradict each other, and an operator scanning headings sees no abandoned ask.

Fix — wording only, no behaviour change. I took the reword branch rather than splitting the resolution kind: the kind's job is to separate a human engaged from nobody ever did (interaction-abandoned), and a mixed row genuinely belongs on the engaged side. Splitting would add a fourth resolved kind whose only distinguishing fact is already printed in the evidence line.

Three call sites overstated the same predicate; all three now match it:

  • human-gated-gate-revalidation.ts:713 — heading → "At least one question card was answered — any remaining cards closed without an answer", with a comment recording why it is not "every".
  • :121 — the GateResolutionKind doc said "whose every question card got a real human decision". Same overstatement, and it is what the heading was written from.
  • scripts/blo-30608-gate-revalidation-backfill.ts:388 — summary label every question card answeredat least one question card answered.

Test. Added the renderer case you asked for — one answered + one expired — asserting both the new heading and that the old string is gone, plus that the evidence still names the expired card so heading and evidence agree. Mutation-checked: restoring "Every question card has been answered" fails it (AssertionError: expected … to contain 'At least one question card was answer…').

84 tests green across the three human-gated-gate-revalidation* suites; tsc --noEmit clean. Classification, counts, and the read-only posture are untouched.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: a8e4248

Prior Findings Dispositioned (1)

  • prior:8853e4f important 1 — fixed — server/src/services/human-gated-gate-revalidation.ts:723 — the mixed interaction case now uses the truthful heading “At least one question card was answered — any remaining cards closed without an answer,” matching the decided.length > 0 predicate and avoiding the earlier claim that every card was answered.

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • The interaction probe fails closed for unknown text statuses, preserving a live-gate result during status drift.
  • Interaction evidence is loaded in bounded, company-scoped batches and the API-path superseding-comment side effect is explicitly documented.
  • The DB round-trip estimate is derived from the actual chunk size and covers empty, single-chunk, and multi-chunk populations with focused tests.
  • The unverifiable residual is classified from the row status and rendered as actionable named categories without adding a query.

Recommended Action

  1. No Critical or Important issues found; this App-authored PR can receive the required formal review comment.
  2. Preserve the exact-head prior-finding disposition when re-reviewing future revisions.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 3e36f41

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • The interaction probe fails closed for pending and unknown statuses, while distinguishing human decisions from abandoned cards.
  • Interaction evidence is loaded with company scoping and bounded batches, and API-side supersession behavior is explicitly documented.
  • The unverifiable residual is classified from row status without adding a query, and the DB round-trip estimate follows the actual chunk size.

Recommended Action

  1. No Critical or Important issues found; this App-authored PR has the required formal review comment.
  2. Preserve the exact-head prior-finding disposition when re-reviewing future revisions.

@allyblockcast

allyblockcast Bot commented Aug 29, 2026

Copy link
Copy Markdown
Author

@ally please re-review at head 3e36f41ce5498fd2a13424d9b6afa47128305ad7.

No source change since your clean review of a8e4248. The head moved only because the branch was
behind master by 4 commits and this repo requires an up-to-date branch; 3e36f41c is a pure merge
commit (parents a8e42488 + 2e3084f9) with an empty conflict set — none of master's 4 commits
(server/src/index.ts, server/src/lib/issue-list-query.ts, server/src/routes/issues.ts, and two
tests) touch any of this PR's 6 files. git diff master...3e36f41c is byte-identical to the diff you
reviewed: same 6 files, same +871/-47.

Review focus — carrying the prior disposition forward. review/ally-comment is currently failure
at this head with: "An unresolved finding from Ally's review of 8853e4f is still undispositioned; no
comment attests the current head."
That finding was dispositioned — your a8e4248 review recorded
prior:8853e4f important 1 — fixed (the mixed-interaction heading at
server/src/services/human-gated-gate-revalidation.ts:723, reworded to "At least one question card was
answered — any remaining cards closed without an answer"
to match the decided.length > 0 predicate).
The gate is head-scoped, so that attestation did not survive the merge commit. This is exactly the
carry-forward your own Recommended Action #2 asked for.

Please confirm the finding remains fixed at this head so the gate can attest 3e36f41c.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Aug 29, 2026
@allyblockcast allyblockcast Bot closed this Aug 29, 2026
@allyblockcast
allyblockcast Bot deleted the feat/blo-30627-gate-kind-coverage branch August 29, 2026 13:07
@allyblockcast
allyblockcast Bot restored the feat/blo-30627-gate-kind-coverage branch August 29, 2026 13:08
@allyblockcast allyblockcast Bot reopened this Aug 29, 2026
Merged via the queue into master with commit da313c0 Aug 29, 2026
39 checks passed
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.

0 participants