Skip to content

Commit 9999bb3

Browse files
authored
Merge pull request #181 from JiusiServe/docs/pr-automation-implementation
Align PR automation RFC with merged contracts and rollout status
2 parents 745f0a8 + c29e660 commit 9999bb3

1 file changed

Lines changed: 138 additions & 79 deletions

File tree

‎doc/RFC-pr-state-machine.md‎

Lines changed: 138 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -346,26 +346,26 @@ recovery, orphan adoption after a crash). Nothing new is invented for writes.
346346
`ready_for_maintainer` requires all of, on the current head:
347347

348348
1. not draft, not parked, not stale;
349-
2. every watched or required check green, `lifecycle.py` classification
350-
`complete == True` (unknown required-set counts as not ready, as today);
349+
2. `lifecycle.py` supplies `ready_verified == True` for this head: the
350+
required-check set is known, required and watched checks are present,
351+
complete and passing, pagination is complete, and required app identities
352+
match.
353+
Missing or pending evidence never counts as green;
351354
3. `mergeable_state == "clean"`;
352355
4. a completed review attempt on this head;
353-
5. **no open blocker/major finding.** Blocker/major findings are carried
354-
forward across heads: each one the bot posted stays on the PR's ledger of
355-
open findings until it is explicitly closed. A finding closes only by one
356-
of:
357-
- a later head's re-review lists it in `finding_dispositions` as
358-
`resolved_or_outdated` **with `head_recheck == "fixed"`**. The Direct
359-
contract also allows `head_recheck == "still_affected"`
360-
(`direct_routing.py:221`, `:893-900`); that value keeps the finding open.
361-
A re-review that simply omits a carried finding does not close it — the
362-
sweep's re-review prompt receives the carried list and must return a
363-
disposition for every entry, and a missing entry counts as
364-
`still_affected`;
365-
- the author marked it disputed: a 👎 reaction on the bot's inline comment
366-
or a reply in its thread. Disputed findings do not block; they are
367-
counted and shown in the ready page ("2 findings disputed by the
368-
author") so the maintainer arrives at exactly the disagreement.
356+
5. **no undisputed open blocker/major finding.** Published blockers remain
357+
in the findings ledger across heads. Only a complete, head-bound recheck
358+
of the carried set with `outcome == "fixed"` closes a finding. A
359+
`still_affected` or `unverified` answer leaves it unresolved; omission,
360+
malformed answers and incomplete coverage cannot resolve it. The request
361+
and result shapes are defined in [the SDK contract below](#sdk-boundary).
362+
363+
An author can dispute a finding with a 👎 reaction on its bot comment
364+
or a reply in its thread. A dispute removes that finding from the ready
365+
gate without proving it fixed. Disputed findings stay in the ledger and
366+
are counted on the ready page so the maintainer sees the disagreement.
367+
Review evidence and dispute state are stored separately; withdrawing a
368+
dispute restores blocking status unless a valid explicit fix remains.
369369

370370
**Disputes are an evidence source with their own deadline, not a
371371
by-product of the next review.** Thread replies arrive through the
@@ -447,19 +447,28 @@ review attempt, and enqueues reviews in this priority order:
447447
2. PRs never reviewed, newest first;
448448
3. PRs pushed since their last review (re-reviews reuse thread dispositions,
449449
so they are the cheapest);
450-
4. legacy backlog (open before the flag turned on), oldest last.
450+
4. legacy backlog (open before the flag turned on), oldest first.
451+
452+
To prevent lower bands from waiting indefinitely, the sweep reserves
453+
`max(1, budget // 5)` slots for eligible heads that have waited at least
454+
one day since first eligibility or their last allocation of review capacity.
455+
At the initial budget this is four slots. The remaining slots follow the
456+
priority bands, and legacy-band work still respects the backfill quota.
457+
The ledger preserves `first_eligible_at` and `last_selected_at` across
458+
restarts and retries; a new head starts a new queue age. Queue eligibility
459+
is refreshed outside the nightly window as well.
451460

452461
Capacity, from the live step medians (`llm` 290 s in Direct mode, `strict`
453462
919 s in Strict mode, one Strict worker):
454463

455464
| Setting | Default | Rationale |
456465
| --- | --- | --- |
457466
| `AUTO_REVIEW_ENABLED` | `false` | shadow first, like every #116 flag |
458-
| `AUTO_REVIEW_AT` | `04:00` (host TZ, UTC+8) | owner's choice; off-peak for CI and for people |
459-
| `AUTO_REVIEW_WINDOW_HOURS` | `6` | sweep must finish before the working day; leftovers carry to the next night at the same priority |
460-
| `AUTO_REVIEW_BUDGET` | `60` | ≈48 new PRs/day plus re-reviews; at 5 min each with 2 Direct workers this is ~2.5 h |
467+
| `AUTO_REVIEW_AT` | `04:00` (`Asia/Shanghai`) | fixed named timezone, independent of host timezone; off-peak for CI and for people |
468+
| `AUTO_REVIEW_WINDOW_HOURS` | `6` | bounded nightly scheduling; unfinished eligible work carries forward with its queue age |
469+
| `AUTO_REVIEW_BUDGET` | `20` | initial measured exposure; increase toward 60 only after at least three successful nights and review of queue, failure and latency measurements |
461470
| `AUTO_REVIEW_MODE` | `direct` | automatic reviews run Direct only; the 50/50 Strict split stays on mention-triggered reviews so the experiment's sample is not swamped |
462-
| `AUTO_REVIEW_BACKFILL_PER_NIGHT` | `10` | legacy backlog (1,037 non-draft open PRs today) drains slowly instead of flooding authors |
471+
| `AUTO_REVIEW_BACKFILL_PER_NIGHT` | `10` | legacy-band reviews are bounded independently; accepted configuration range is 0–10 |
463472

464473
The mention trigger stays exactly as it is, as the immediate manual path, and
465474
a mention-triggered review counts against no nightly budget.
@@ -660,8 +669,10 @@ Reconciler contract:
660669
re-run on the same SHA re-derives the same keys and is a no-op.
661670

662671
Existing evidence tables remain authoritative; additive migrations preserve claims and history. `workflow_jobs`, `review_attempts`,
663-
`lifecycle_notes`, `stale_windows` and `applied_labels` remain the evidence;
664-
`pr_state` is what the kanban, the pager and the clocks read.
672+
`lifecycle_notes`, `stale_windows`, `idle_ladder_windows` and historical
673+
`applied_labels` remain evidence. Current internal labels use
674+
`inferred_labels`, and `auto_review_queue` retains eligibility/service age;
675+
`pr_state` is the derived projection for the kanban, pager and clocks.
665676

666677
## Kanban
667678

@@ -677,45 +688,67 @@ blocked, timed out, rerouted all land in a column).
677688
| Concern | Owner |
678689
| --- | --- |
679690
| projection, reconciler, sweep, clocks, our labels, notices, dashboard | `omni-reviewbot` |
680-
| review verdict, severities, `finding_dispositions`, quality verdict, head binding — the per-head facts | this repo, via the SDK (`build_review_result`, `build_quality_result`) |
691+
| review verdict, published `findings`, explicit `finding_rechecks`, quality verdict and head binding | this repo, via the SDK (`build_review_result`, `build_quality_result`) |
681692
| `open_findings` / `disputed_findings` counts | the bot, because they combine SDK per-finding results with reaction and reply data only the bot reads |
682693

683-
**SDK change required (this repo, a contract version bump).** Today
684-
`build_review_result` projects each disposition record onto three fields —
685-
`anchor`, `disposition`, `declared` (`contract.py:81`, `sanitize_dispositions`)
686-
— and the Direct completion decision reports aggregate counts only. Neither
687-
carries what the ready gate needs to retire a carried blocker. The review
688-
result gains a head-bound, versioned per-finding list:
694+
### SDK boundary
695+
696+
**Implemented SDK contract.** [Copilot #180](https://github.com/JiusiServe/InferMatrixCopilot/pull/180)
697+
ships Direct `1.1.0` and Strict `1.3.0` through the public
698+
`infermatrix_copilot.sdk.v1` surface. Published findings and carried-finding
699+
rechecks are separate data sets, defined by
700+
[`sdk/v1/models.py`](../src/infermatrix_copilot/sdk/v1/models.py),
701+
[`sdk/v1/rechecks.py`](../src/infermatrix_copilot/sdk/v1/rechecks.py) and
702+
[`contract.py`](../src/infermatrix_copilot/contract.py):
689703

690704
```text
691-
findings: [
692-
{ finding_id, # stable across heads: hash(rule_id or normalized title, anchor path, symbol)
693-
severity, # blocker | major | minor | nit, after demotion
694-
anchor, # path + line range on head_sha, or null (general finding)
695-
comment_id, # GitHub inline comment the bot posted for it, when published
696-
disposition, # new | duplicate | extends_existing | resolved_or_outdated
697-
head_recheck, # fixed | still_affected | null (only for resolved_or_outdated)
698-
carried_from, # finding_id on the previous reviewed head, or null
699-
head_sha }
705+
carried_findings: [ # DirectReviewRequest / StrictReviewRequest
706+
{ finding_id, source_head_sha, severity, path, title,
707+
body, line, disputed }
708+
]
709+
finding_rechecks: [ # DirectCompletionRequest / Strict result
710+
{ finding_id, head_sha, outcome, evidence }
711+
] # outcome: fixed | still_affected | unverified
712+
findings: [ # published findings in the Strict result
713+
{ finding_id, severity, anchor, head_sha }
700714
]
701715
```
702716

703-
The sweep's re-review passes the carried open findings (id, severity,
704-
anchor, comment id) into the run's context, and the SDK result must return
705-
one entry per carried id; a missing entry is treated as `still_affected`
706-
by the bot. A contract test in this repo fixes a carried blocker on a new
707-
head and asserts the result closes it (`resolved_or_outdated` + `fixed`),
708-
and a second test asserts that `still_affected` and omission both leave it
709-
open. The existing aggregate `finding_dispositions` field stays for
710-
compatibility.
717+
Finding identity derives from path and normalized finding text, excluding
718+
line number and head. Publication comment IDs and consumer revision counters
719+
belong to ReviewBot's ledger, not the provider's finding records. The
720+
existing `finding_dispositions` audit remains separate and does not prove a
721+
carried finding fixed.
722+
723+
Direct binds the issued review context to the carried set and expected
724+
head, then validates explicit rechecks at completion. Strict persists the
725+
carried request and rechecks the findings against its frozen checkout;
726+
`build_review_result` returns `finding_rechecks`, `rechecks_complete` and
727+
`recheck_missing`, including gaps when a run fails before review. Validation
728+
rejects unknown/duplicate IDs, wrong-head answers and empty evidence. Every
729+
carried ID needs a valid answer; `unverified` is an answer that leaves the
730+
finding unresolved, while omission is a coverage gap.
731+
732+
[ReviewBot #85](https://github.com/JiusiServe/omni-reviewbot/pull/85) pins the
733+
provider to `745f0a83e20ae3f5c5868b3504f4ef10c6284887` and consumes this
734+
contract in Direct and Strict reviews. It retains a carried snapshot across
735+
Strict retries, independently validates coverage before accepting fixes,
736+
and guards fixes against stale review revisions. Unresolved answers remain
737+
conservative when reviews race. Its migration reopens findings previously
738+
closed by omission and invalidates that old readiness evidence. A dispute
739+
remains independent of the underlying recheck outcome.
711740

712741
The copilot stays stateless per run. Re-review after a push is an ordinary
713742
`pr_review` run with the existing thread context; no new task kind.
714743

715744
## Rollout
716745

717-
Every step is a flag flip on the host; nothing is posted to GitHub until the
718-
named flag is on. Each step runs at least three nights before the next.
746+
Every stage is enabled explicitly on the host; new feature comments require
747+
their named flag. Check each stage against its exit criteria before advancing,
748+
and keep the initial exposure until at least three successful nightly runs
749+
have been observed. Merged code and passing CI do not constitute rollout
750+
acceptance. Record the exact provider/bot pair, effective flags, observed
751+
results and pending criteria for every stage.
719752

720753
1. **Shadow.** `PR_STATE_ENABLED=true`, everything else off. Tables fill, the
721754
kanban shows states, our labels are recorded. Exit criterion: the
@@ -726,18 +759,21 @@ named flag is on. Each step runs at least three nights before the next.
726759
here and no repository labels to create, because nothing is applied to
727760
GitHub. Exit: a maintainer reading the board agrees with the labels on
728761
20 sampled PRs.
729-
3. **Sweep.** `AUTO_REVIEW_ENABLED=true` at budget 20, then 60. Exit: sweep
730-
finishes inside the window on three consecutive nights; review failure rate
731-
no worse than mention-triggered reviews (today 14 failed of 105).
762+
3. **Sweep and disputes.** Enable `AUTO_REVIEW_ENABLED=true` at budget 20
763+
and bounded `DISPUTE_SCAN_ENABLED=true`. Increase toward 60 only after
764+
the three-night observation requirement and measured results justify it.
765+
Exit: sweep finishes inside the window on three consecutive nights;
766+
review failure rate is no worse than mention-triggered reviews, measured
767+
over the same observation period.
732768
4. **Ready page.** `READY_PAGE_ENABLED=true`, baselined so the PRs already
733769
ready are recorded without paging, and with the lifecycle
734770
`ready_to_merge` cutover above in the same release — the old emitter
735771
retires as the new one starts, never both running. Exit: a maintainer sampling 20 ready
736772
pages finds ≤ 2 that should not have been paged (false-ready ≤ 10 %).
737773
5. **The idle ladder.** `IDLE_LADDER_ENABLED=true`; the 14- and 30-day
738-
notices join the existing 7-day reminder. This is the only step that adds outward noise, so it goes last
739-
and is announced first: 715 PRs are already past 7 days, they arrive at
740-
10 per cycle, and each gets the highest rung only.
774+
notices join the existing 7-day reminder. This adds follow-ups to the
775+
inactive backlog, so it goes last. All rungs share the existing
776+
10-comment cycle budget, and backlog PRs get only the highest passed rung.
741777

742778
### Rollback
743779

@@ -795,24 +831,44 @@ The owner approved the implementation and staged rollout on 2026-09-22:
795831

796832
## Implementation checklist (2026-09-22)
797833

798-
- [x] Projection/transition ledger and nightly review sweep shipped flag-off.
799-
- [x] Finding ledger, deadline-first visits, dispute scanning and independent
800-
PR snapshot refresh shipped flag-off (ReviewBot #73–#76).
801-
- [x] Retry path recognizes the watcher's `timeout` status.
802-
- [ ] Remove all non-comment writes from both runtime surfaces, including
803-
label writers, reaction acknowledgements and knowledge PR creation.
804-
- [ ] Correct projection: only GitHub facts produce draft/closed; day 30 is
805-
a closure recommendation on an open idle PR.
806-
- [ ] Carry finding IDs through explicit rechecks; omission never fixes a
807-
blocker. Reconcile previously omission-resolved rows before paging.
808-
- [ ] Validate bounded automatic reviews, retries, backlog fairness and
809-
current-head publication against the pinned provider.
810-
- [ ] Implement epoch/entry ready notifications and the lifecycle cutover.
811-
- [ ] Extend `stale.py` with the 14/30-day rungs and crash-safe deduplication.
812-
- [ ] Surface independent evidence, next actor, pending delivery and queue
813-
health on the board.
814-
- [ ] Deploy the tested provider/bot pair, then record rollout observations
815-
and enable stages only after their exit criteria pass.
834+
This is a source/CI checkpoint as of 10:24 UTC. Checked items identify merged
835+
implementation, not enabled production behavior. The runtime acceptance
836+
checklist below stays open until deployment observations establish it.
837+
838+
- [x] Projection/transition ledger, initial nightly sweep, finding ledger,
839+
deadline-first visits, dispute scanning and independent PR snapshot
840+
refresh merged flag-off (ReviewBot #73–#76).
841+
- [x] Retry path recognizes the watcher's `timeout` status; retained in the
842+
automatic-sweep regression coverage.
843+
- [x] Comment-only runtime authority, including internal labels, removed
844+
reactions and local-only knowledge exports:
845+
[ReviewBot #82](https://github.com/JiusiServe/omni-reviewbot/pull/82) and
846+
[Copilot #179](https://github.com/JiusiServe/InferMatrixCopilot/pull/179).
847+
- [x] Only GitHub facts produce draft/closed; day 30 remains a recommendation
848+
on an open idle PR. Independent evidence and next actor are retained:
849+
[ReviewBot #84](https://github.com/JiusiServe/omni-reviewbot/pull/84).
850+
- [x] Explicit carried-finding rechecks, omission migration, independent
851+
disputes and compatible provider pin:
852+
[Copilot #180](https://github.com/JiusiServe/InferMatrixCopilot/pull/180)
853+
and [ReviewBot #85](https://github.com/JiusiServe/omni-reviewbot/pull/85).
854+
- [x] Current-head verified CI proof is required for readiness:
855+
[ReviewBot #86](https://github.com/JiusiServe/omni-reviewbot/pull/86).
856+
- [x] Initial budget 20, explicit Asia/Shanghai window, bounded backlog,
857+
durable queue ages and reserved service for older waiting work:
858+
[ReviewBot #88](https://github.com/JiusiServe/omni-reviewbot/pull/88).
859+
- [x] Extend `stale.py` with the 14/30-day rungs, highest-passed backlog
860+
behavior, shared budget, uncertain-delivery recovery and re-enable
861+
coverage: [ReviewBot #87](https://github.com/JiusiServe/omni-reviewbot/pull/87).
862+
- [ ] Finish epoch/entry ready notifications and durable lifecycle cutover;
863+
implementation is under review, with runtime validation still pending.
864+
- [ ] Finish independent evidence, next actor, pending delivery and queue
865+
health on the board:
866+
[ReviewBot #89](https://github.com/JiusiServe/omni-reviewbot/pull/89)
867+
is under review at this checkpoint.
868+
- [ ] Deploy the tested provider/bot pair and verify representative states.
869+
- [ ] Record stage observations, enable stages only after their exit criteria
870+
pass, and observe at least three successful nights before increasing
871+
exposure. No runtime-acceptance item is satisfied by this checklist.
816872

817873
## Risks
818874

@@ -927,10 +983,13 @@ The owner approved the implementation and staged rollout on 2026-09-22:
927983
finding's original comment from an older head is found after a push
928984
because the scan follows the stored comment
929985
id.
930-
- Contract tests in this repo: the per-finding result carries `finding_id`,
931-
`severity`, `head_recheck` and `carried_from` across the SDK boundary; a
932-
carried blocker closes only with `resolved_or_outdated` + `fixed`, and
933-
stays open on `still_affected` or omission.
986+
- Contract tests in this repo: Direct and Strict carry typed finding IDs
987+
and source heads; rechecks carry the current head, an explicit outcome
988+
and evidence. Cover complete `fixed`, `still_affected`, `unverified`,
989+
omission, duplicate/unknown IDs, wrong-head answers and early run failure.
990+
Consumer tests prove only complete explicit fixes close carried blockers
991+
and that concurrent reviews or dispute changes cannot erase unresolved
992+
evidence.
934993

935994
## Metrics for success
936995

0 commit comments

Comments
 (0)