This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/doris-skills.git


The following commit(s) were added to refs/heads/main by this push:
     new 028ba92  feat(doris-repo-review): carry review state across runs, add 
coverage report and premise checks (#10)
028ba92 is described below

commit 028ba927e1d9052c987156e6db1b03c6ed8856cc
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Thu Aug 20 12:54:36 2026 +0800

    feat(doris-repo-review): carry review state across runs, add coverage 
report and premise checks (#10)
    
    A PR is normally reviewed more than once, but every run used to start from 
zero:
    the dismissals were re-derived, the same files were re-read, and "was this
    considered last time?" was unanswerable. This teaches the skill to inherit 
from
    its own earlier runs, and closes three gaps that made a round cost more 
than it
    returned.
    
    Scripts:
    
    - `coverage-report.sh` (new, step 6a): mechanical check of which changed 
files
      no ledger file has mentioned yet. Exits 1 when gaps remain and appends to
      `coverage_history.tsv`. The risk scan and the merged ledger are excluded 
--
      neither is evidence that a file was read.
    - `save-run-state.sh` (new, step 11): persists the merged ledger under a 
stable
      per-PR state directory (`$DORIS_REVIEW_STATE`, default
      `~/.cache/doris-repo-review`) and maintains an `index.tsv` history. Runs 
for
      both verdicts.
    - `prepare-review-context.sh`: emits `coverage_checklist.tsv`, loads earlier
      runs of the same PR into `$CTX/prior_runs/`, and records `STATE_DIR` /
      `PRIOR_RUNS` in `meta.env`.
    
    SKILL.md:
    
    - 0.4a: evidence may live outside the repository -- open the pinned jar, the
      sources jar, the vendored definition, and name the artifact and its 
version.
    - 2.1 / 3.3a: prior runs are input, not history. Dismissals carry forward 
with
      their evidence; a rebased head is compared by content, not by commit hash.
    - 4.1: every risk item carries a premise and a one-command premise check, 
run
      before dispatch. A false premise becomes a dismissal, not a subagent.
    - 5.1: round 1 goes out in two waves, because the ledger is empty when it
      starts and the deduplication rule cannot work against it.
    - 7: a round converges on no new Blocker/Major plus clean coverage, not on 
"no
      new candidates at all" -- a run still yielding Minor/Nit has converged.
    - 9: documents are named by reviewed head 
(`pr-<N>-review.<head7>.{en,zh}.md`)
      with symlinks to the newest pair; an earlier run's pair is never 
overwritten.
    - 13: six new traps drawn from the above.
    
    References:
    
    - `prompts.md`: `{TECHNIQUE}` placeholder plus section D, a catalogue of the
      eight techniques that actually produce findings, so a subagent is aimed
      rather than left to invent "read the diff and look for mistakes".
    - `doc-templates.md`: `Builds on` header row, convergence stated separately
      from the verdict, and three additions to "Coverage and Limits" (outside
      evidence, inherited runs, convergence) in both languages.
    
    README: the skill's one-line description now names the new mechanics.
    
    
    Claude-Session: https://claude.ai/code/session_01GWPzxwYKXgPKrsXVSazJnr
    
    Co-authored-by: morningman <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 README.md                                          |   2 +-
 skills/doris-repo-review/SKILL.md                  | 235 +++++++++++++++++++--
 .../doris-repo-review/references/doc-templates.md  |  32 ++-
 skills/doris-repo-review/references/prompts.md     |  76 +++++++
 .../doris-repo-review/scripts/coverage-report.sh   | 115 ++++++++++
 .../scripts/prepare-review-context.sh              |  42 ++++
 skills/doris-repo-review/scripts/save-run-state.sh |  86 ++++++++
 7 files changed, 562 insertions(+), 26 deletions(-)

diff --git a/README.md b/README.md
index 0ff7565..5c23ff6 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ skill from its `description`, so in practice you describe the 
problem and the ri
 
 | Skill | What it does | Use it when |
 |---|---|---|
-| [`doris-repo-review`](skills/doris-repo-review/) | Contributor-side PR 
review of a local `apache/doris` clone, running the same flow as the CI Code 
Review Runner: worktree alignment, risk scan, subagent fan-out, shared ledger, 
anchored EN + ZH review documents, and — on a passing review — one 
machine-readable PASS comment on the PR | Reviewing an `apache/doris` pull 
request locally, the way the pipeline would |
+| [`doris-repo-review`](skills/doris-repo-review/) | Contributor-side PR 
review of a local `apache/doris` clone, running the same flow as the CI Code 
Review Runner: worktree alignment, risk scan with premise checks, subagent 
fan-out, shared ledger, mechanical coverage report, anchored EN + ZH review 
documents kept per reviewed head, dismissals carried forward from earlier 
reviews of the same PR, and — on a passing review — one machine-readable PASS 
comment on the PR | Reviewing an `apach [...]
 
 These skills read a Doris **source** checkout and never touch a cluster; the 
cluster-side skills
 never touch repository source. Nothing here builds Doris or runs tests. The 
one GitHub write in
diff --git a/skills/doris-repo-review/SKILL.md 
b/skills/doris-repo-review/SKILL.md
index c460e38..3a1a02f 100644
--- a/skills/doris-repo-review/SKILL.md
+++ b/skills/doris-repo-review/SKILL.md
@@ -34,16 +34,20 @@ Let `$S` be this skill's own `scripts/` directory (with a 
default Claude Code in
 $S/align-to-pr.sh <PR> --check                      # diagnosis only, changes 
nothing
 $S/align-to-pr.sh <PR> --out "$CTX/align.env"       # step 1: align the 
current directory
 $S/prepare-review-context.sh --ctx "$CTX" --align "$CTX/align.env"   # step 2: 
gather context
+$S/coverage-report.sh --ctx "$CTX"                  # step 6a: what has nobody 
read yet
 python3 $S/verify-anchors.py --ctx "$CTX" --doc <en> --doc <zh>      # step 9: 
verify anchors
 $S/post-pass-comment.sh --ctx "$CTX" --model <id> ... --dry-run      # step 
10: PASS comment
+$S/save-run-state.sh --ctx "$CTX" --verdict ... --findings b,m,mi,n  # step 
11: leave state for next time
 ```
 
 | File | Purpose |
 |---|---|
 | `scripts/align-to-pr.sh` | Resolve the PR, diagnose how the current 
directory relates to it, align it to the PR head |
 | `scripts/prepare-review-context.sh` | Produce the authoritative diff, 
new-side line ranges, required AGENTS.md list, existing comments, ledger 
skeleton |
+| `scripts/coverage-report.sh` | Mechanical check of which changed files no 
ledger file has mentioned yet |
 | `scripts/verify-anchors.py` | Check that every `path:line` anchor really 
exists and that both documents expose the same finding IDs |
 | `scripts/post-pass-comment.sh` | Render and post the machine-readable PASS 
comment; refuses everything that is not a pass |
+| `scripts/save-run-state.sh` | Persist this run's merged ledger under the 
stable per-PR state directory, so the next review inherits its dismissals |
 | `references/prompts.md` | Subagent prompt templates (CI wording, carried 
over verbatim) |
 | `references/doc-templates.md` | Templates for both documents, anchor format, 
verdict rule |
 | `references/pr-comment-format.md` | The `doris-repo-review/v1` comment 
schema, field meanings, and how a program reads it back |
@@ -75,6 +79,15 @@ diff from the merge base.
 4. **Confirm a path before reading it.** If a path is not already confirmed by
    `pr_changed_files.txt`, `pr.diff`, or the output of an earlier successful 
command, run
    `rg --files` to confirm it first.
+4a. **Evidence may live outside the repository, and you are expected to go and 
get it.** Rule 3
+   fixes where the *change list* comes from; it says nothing about where 
*evidence* comes from.
+   When a dependency's own content is what decides the behaviour under review, 
read that
+   dependency: unzip the jar in `~/.m2/repository/...` that the build actually 
pins and read the
+   resource inside it, read the `-sources.jar` of the library whose semantics 
the change relies on,
+   read the vendored service definition, read the JDK class whose contract a 
comment claims.
+   A review that never leaves the diff cannot find a defect whose two halves 
are "the changed line"
+   and "what the changed line now reaches" - which is exactly where the worst 
findings live.
+   Always name the artifact and its version in the finding, so the evidence is 
reproducible.
 5. **Do not stop at the first blocking issue.** Work through the changed 
files, the related
    control flow, the tests, and the parallel or special-case paths.
 6. **Every suspicion must reach a conclusion**: it becomes a finding, is 
excluded as "already
@@ -157,10 +170,45 @@ Output (under `$CTX`):
 | `review_focus.txt` | The user's focus points |
 | `worktree_status.txt` | Uncommitted changes in the review tree (**out of 
review scope**; call them out in the documents) |
 | `ledger/` | Shared-ledger skeleton |
+| `coverage_checklist.tsv` | One row per changed file, for the mechanical 
coverage report of step 6a |
+| `prior_runs/` | What earlier reviews of **this same PR** concluded - see 
below |
 
 If `BASE_SOURCE` is not `PR base sha (matches CI)`, the baseline differs from 
CI's and the
 documents must say so.
 
+### 2.1 Prior runs of the same PR are input, not history
+
+A PR is usually reviewed more than once - the author pushes, you re-run. Each 
run used to start
+from zero, re-deriving the same dismissals and re-reading the same files, and 
none of it survived.
+That is the single largest source of wasted budget, and it is why "why did the 
last review not
+find this?" used to be unanswerable.
+
+`prepare-review-context.sh` therefore keeps every run's merged ledger under a 
**stable per-PR
+state directory**, outside the session scratchpad and outside the repository:
+
+```
+${DORIS_REVIEW_STATE:-${XDG_CACHE_HOME:-$HOME/.cache}/doris-repo-review}/<owner>-<repo>/pr-<N>/
+    runs/<head7>/main-merged.md      one per reviewed head
+    runs/<head7>/meta.env
+    index.tsv                        head sha, date, verdict, finding counts
+```
+
+and copies whatever it finds into `$CTX/prior_runs/`. **Read it as part of 
step 3**, right after
+`pr_review_threads.md`, and treat it exactly the way you treat an existing 
review thread:
+
+- Its **"Considered and Dismissed" table is the expensive part.** A dismissal 
that still holds does
+  not need re-deriving; carry it forward into this run's table with its 
original evidence and a
+  note that it was re-confirmed (or say why it no longer holds). Only 
re-derive a dismissal whose
+  premise the new head could have changed.
+- Its accepted findings tell you what the author has already been told. One 
that is now fixed is
+  worth one line in the closing report ("fixed at this head"), not a finding.
+- **If a prior run reviewed a different head, say so and check what moved.** 
The branch may have
+  been rebased, so comparing by commit hash is unsafe: compare content
+  (`git show <old head>:<path>`) before claiming a finding is new.
+
+An empty `prior_runs/` means this is the first review of this PR - not that 
nothing was ever
+reviewed. Say which it is in "Coverage and Limits".
+
 ---
 
 ## 3. Required reading (in this order)
@@ -176,6 +224,10 @@ Before looking at any code, the main agent reads, in order:
    raise** the same or a substantially similar issue again, even phrased 
differently. Raise a
    similar concern only when this PR introduces a genuinely different instance 
somewhere else that
    the existing comments do not cover, and say why it is distinct.
+3a. Everything under `$CTX/prior_runs/` - what earlier reviews of this same PR 
concluded. See
+   section 2.1: dismissals carry forward with their evidence, accepted 
findings that are now fixed
+   become one line in the closing report, and a prior run on a different head 
means comparing
+   content rather than commit hashes.
 4. `$CTX/review_focus.txt` - do the full review as usual and pay extra 
attention to these points;
    the final documents must respond to each one, even if the conclusion is "no 
additional issue
    found for this point".
@@ -193,12 +245,41 @@ points look risky to you?
 
 Write the result into `$CTX/ledger/00-main-risk-scan.md`, each entry carrying: 
an ID, the changed
 files/lines involved, the related mechanism to inspect, why it is suspicious, 
the upstream or
-downstream files that must be read alongside it, and the **specific question** 
a risk-focused
-subagent has to answer.
+downstream files that must be read alongside it, the **specific question** a 
risk-focused
+subagent has to answer, and - see below - a **premise check**.
 
 An empty risk scan means you have not understood the PR yet - go back and read 
it; do not skip
 this step.
 
+### 4.1 Every risk item carries a premise check, and you run it before 
spawning anything
+
+A risk item always rests on a premise: "this used to be X and is now Y", 
"nothing validates Z",
+"only one caller does W". If the premise is false the subagent spends its 
entire budget proving
+you wrong, and you learn nothing you could not have learned in thirty seconds.
+
+So each entry gets one more field:
+
+```
+  Premise:        <the one factual claim the whole item rests on>
+  Premise check:  <a single command that confirms or kills it>
+  Premise result: <confirmed | FALSE - item dismissed | cannot be checked 
cheaply>
+```
+
+Run every one of them **before** step 5. The check is nearly always a 
one-liner against the base:
+
+```bash
+git show "$BASE_SHA:path/to/File.java" | grep -n 'thing I think is new'
+git show "$BASE_SHA:path/to/File.java" | sed -n '120,140p'
+rg -n 'symbol' --files-with-matches            # "only one caller" claims
+```
+
+A premise that comes back FALSE does not become a subagent - it becomes a row 
in "Considered and
+Dismissed" with the command as its evidence. That row is worth as much as a 
finding: it is the
+part of the review that says "this was checked", and it costs one command 
instead of one agent.
+
+A premise you genuinely cannot check cheaply is fine - dispatch it, and say in 
the prompt that the
+premise is unverified so the subagent checks it first and stops early if it 
fails.
+
 ---
 
 ## 5. Spawn the subagents
@@ -219,13 +300,41 @@ see the bug. Give every subagent its own ledger file, 
`$CTX/ledger/sub-<round>-<
 Take the prompts from `references/prompts.md`: section A (shared preamble) 
plus section B
 (full review) or section C (risk-focused), substituting `{CTX}` / 
`{REPO_ROOT}` (= `$WORKDIR`) /
 `{BASE_SHA}` / `{HEAD_SHA}` / `{ROUND}` / `{AGENT_ID}` / `{FOCUS}`. **Every 
subagent prompt must
-state the ledger directory and the path of that subagent's own file.**
+state the ledger directory and the path of that subagent's own file.** Section 
D of that file is a
+catalogue of the techniques that actually find things - name the one you want 
in each prompt
+rather than leaving the subagent to invent a method.
 
 > **One implementation difference from CI**: CI uses a single 
 > `subagent_review_findings.md` with
 > sections; locally, concurrent writes to one file collide on patch, so this 
 > becomes **one file per
 > owner** under `ledger/`. The semantics are unchanged - a single shared 
 > source of truth, everyone
 > reads all of it, each writes only their own, the main agent merges.
 
+### 5.1 Round 1 goes out in two waves, because the ledger is empty when it 
starts
+
+The deduplication rule ("read every file in `ledger/` before reviewing") 
cannot work in round 1:
+every subagent starts at the same instant against an empty directory. In 
practice three agents
+independently rediscover the same defect and three budgets buy one finding.
+
+So round 1 - and only round 1 - is dispatched in two waves:
+
+- **Wave A: the full-review subagents.** All of them concurrently, as before. 
They own the slices,
+  so their coverage is what the union has to span.
+- **Wave B: the risk-focused subagents.** Dispatched once wave A returns, 
concurrently among
+  themselves. They read wave A's ledger files first, so a mechanism wave A 
already settled becomes
+  a duplicate note instead of a second investigation, and a risk item wave A 
has already answered
+  is dropped before it costs anything.
+
+Two things make this cheap rather than slow. Wave A is the long pole either 
way. And the main
+agent is not idle in between - it merges wave A (step 6) while wave B runs.
+
+Where two waves genuinely will not fit - a small PR, or a risk item so 
specific that no
+full-review slice touches it - dispatch everything at once and **pre-seed the 
ledger instead**:
+before spawning, write into `00-main-risk-scan.md` the defects you already 
expect each slice to
+surface, so a concurrent agent can recognise one of yours and mark it 
duplicate rather than
+writing it up from scratch.
+
+From round 2 onward the ledger is populated, so all subagents of a round go 
out together.
+
 ---
 
 ## 6. Main agent merges, verifies, deduplicates
@@ -245,19 +354,53 @@ candidate into `$CTX/ledger/main-merged.md`:
 
 When this step ends, **no candidate may be left without a status**.
 
+### 6a. Run the coverage report at the end of every round
+
+```bash
+$S/coverage-report.sh --ctx "$CTX"
+```
+
+It is mechanical, not a judgement: it walks `pr_changed_files.txt` and reports 
which changed files
+no ledger file has so much as mentioned. Finding "13 changed files nobody 
opened" is a job for
+`grep`, not for a subagent in round 3 - and knowing it after round 1 is what 
lets round 2 be
+aimed instead of guessed.
+
+Its output is an input to the next round's slicing, and to the verdict: **a 
file nobody read is
+not a file with no findings.** Either cover it in the next round or read it 
yourself in step 8.
+
 ---
 
 ## 7. Convergence loop (at most 3 rounds)
 
-One round = step 5 + step 6. Record the outcome in the `Convergence Rounds` 
table of
-`main-merged.md`.
+One round = step 5 + step 6 + step 6a. Record the outcome in the `Convergence 
Rounds` table of
+`main-merged.md`, one row per round with: subagents, new candidates **by 
severity**, coverage gaps
+remaining, verdict.
+
+**Convergence is about the verdict, not about the count.** The rounds exist to 
make you confident
+in the answer you are about to give - which findings block the merge - not to 
empty the well of
+Nits. A large PR will yield another Minor for as long as you keep looking, and 
a rule that waits
+for that to stop combined with the re-slicing rule below can never terminate.
+
+So a round **converges** when both hold:
+
+- it produced **no new `Blocker` and no new `Major`**, and
+- the step-6a coverage report is clean - every changed file has been read by 
somebody.
+
+Then go to step 8, and record in the documents that the verdict has been 
stable since round N.
 
-- Every subagent returned `NO_NEW_VALUABLE_FINDINGS` → go to step 8.
-- New valuable candidates remain → start another round: **re-slice the 
coverage** based on what
-  this round taught you (do not re-dispatch the same split unchanged), and add 
risk-focused
-  subagents for any newly suspicious mechanism.
-- **Cap of 3 rounds.** If the cap is reached and new candidates still appear, 
finish normally but
-  state in the verdict and in "Coverage and Limits" that **this review did not 
converge**.
+Otherwise start another round: **re-slice the coverage** based on what this 
round taught you (do
+not re-dispatch the same split unchanged), and add risk-focused subagents for 
any newly suspicious
+mechanism. Aim the new round at where severity actually came from, not at what 
is left over.
+
+- **Cap of 3 rounds.** If the cap is reached while `Blocker`/`Major` 
candidates are still
+  appearing, or while coverage gaps remain, finish normally but state in the 
verdict and in
+  "Coverage and Limits" that **this review did not converge**, and say which 
of the two conditions
+  failed.
+- If the cap is reached with only `Minor`/`Nit` still trickling in, that is 
**converged**, and the
+  documents should say so plainly: "the verdict was settled at round N; later 
rounds added only
+  Minor and Nit findings". Do not report a converged review as a failed one.
+- **A round that returns only Nits is a stop signal, not a reason for another 
round.** Record the
+  yield (agents spent, findings by severity) so the next person can see where 
the returns fell off.
 
 ---
 
@@ -266,8 +409,11 @@ One round = step 5 + step 6. Record the outcome in the 
`Convergence Rounds` tabl
 Before writing the documents, walk explicitly through the changed-file list 
and the open-candidate
 list:
 
-- Was every changed file covered by at least one subagent? Cover the rest 
yourself.
+- Run `$S/coverage-report.sh --ctx "$CTX"` one last time. Was every changed 
file covered by at
+  least one subagent? Cover the rest yourself, and say in "Coverage and 
Limits" which files you
+  read only here.
 - Does every suspicion have a conclusion?
+- Did every dismissal carried forward from `prior_runs/` get re-confirmed or 
re-opened?
 - Does every applicable item in Part 1.3 of the code-review skill have an 
explicit conclusion?
 - Is there anywhere you are still unsure about, or that may not have been 
investigated deeply
   enough? Investigate it now.
@@ -278,13 +424,25 @@ Only after the sweep may you write the documents.
 
 ## 9. Produce the two documents
 
-Write them into `review-docs/` in the **current directory** (`mkdir -p` it if 
needed):
+Write them into `review-docs/` in the **current directory** (`mkdir -p` it if 
needed), named by the
+head that was actually reviewed:
 
 ```
-review-docs/pr-<N>-review.en.md
-review-docs/pr-<N>-review.zh.md
+review-docs/pr-<N>-review.<head7>.en.md      e.g. pr-66770-review.3f45815.en.md
+review-docs/pr-<N>-review.<head7>.zh.md
+review-docs/pr-<N>-review.en.md              symlink -> the newest of the above
+review-docs/pr-<N>-review.zh.md              symlink -> the newest of the above
 ```
 
+**Never overwrite an earlier run's document.** The head sha is in the name 
precisely so a re-review
+cannot destroy what the last one concluded: that record is the only way to 
answer "was this
+considered last time, and dismissed with what evidence?" - and once it is 
gone, it is gone, because
+`review-docs/` is untracked. The two unsuffixed names are convenience 
symlinks, so anything that
+links to them keeps working while the history accumulates behind them.
+
+Point `verify-anchors.py` at the real files, not the symlinks. If a document 
for this exact head
+already exists, you are re-running against an unchanged head: overwrite that 
one, and only that one.
+
 `references/doc-templates.md` holds the templates, the anchor format, and the 
verdict rule
 (`Blocker`/`Major` → REQUEST_CHANGES; only `Minor`/`Nit` → APPROVE). The 
essentials:
 
@@ -302,8 +460,11 @@ review-docs/pr-<N>-review.zh.md
 - "Response to Review Focus" answers each of the user's focus points.
 - "Considered and Dismissed" lists every excluded suspicion together with its 
evidence.
 - "Coverage and Limits" states: what was read in depth versus skimmed, how the 
subagents were
-  split, what was not verified locally (builds, tests, anything needing a real 
cluster), and the
-  uncommitted changes from `worktree_status.txt` that were excluded.
+  split, what was not verified locally (builds, tests, anything needing a real 
cluster), the
+  uncommitted changes from `worktree_status.txt` that were excluded, **which 
earlier runs of this
+  PR this one builds on** (heads and dates from `prior_runs/`, or "first 
review of this PR"), and
+  **whether the review converged** in the sense of step 7 - naming the round 
after which the
+  verdict stopped moving, and, if it did not converge, which of the two 
conditions failed.
 
 When they are written, running the verifier is **mandatory**; if it fails, fix 
the documents and
 re-run until it passes:
@@ -368,7 +529,12 @@ $S/post-pass-comment.sh --ctx "$CTX" \
 Tell the user:
 
 1. The paths of both documents, the verdict (REQUEST_CHANGES / APPROVE), the 
finding count per
-   severity, and whether the rounds converged.
+   severity, and whether the rounds converged in the sense of step 7 - "the 
verdict was settled at
+   round N" if they did, and which condition failed if they did not. A run 
that ended with only
+   Minor/Nit still arriving **converged**; do not report it as a failure.
+1a. **What this run inherited**, when `prior_runs/` was not empty: which heads 
were reviewed
+   before, how many of their dismissals were carried forward, and which of 
their accepted findings
+   are fixed at this head. If no prior run existed, say that this is the first 
review of this PR.
 2. The `branch check` / `commit check` results - especially `ahead:N` 
(unpushed commits that were
    not reviewed).
 3. **What happened to the PASS comment**: the URL when one was posted or 
updated, or the reason
@@ -378,6 +544,19 @@ Tell the user:
    may still want to read the code.
 5. `review-docs/` **is not gitignored in the doris repository**, so **do not 
commit it
    automatically**; leave that to the user.
+6. **Save the run state before you finish** - this is what makes the next 
review cheaper and makes
+   this one auditable:
+
+   ```bash
+   $S/save-run-state.sh --ctx "$CTX" \
+       --verdict REQUEST_CHANGES --findings <blocker>,<major>,<minor>,<nit> \
+       --rounds <r> --converged <true|false> \
+       --docs "review-docs/pr-<N>-review.<head7>.en.md" --note "<one line: the 
headline finding>"
+   ```
+
+   It runs for **both** verdicts - a REQUEST_CHANGES review has just as much 
to hand forward as a
+   pass, and rather more. Tell the user where it was saved and print the 
per-PR history it echoes,
+   so the sequence of reviews of this PR is visible in one place.
 
 ---
 
@@ -393,7 +572,8 @@ Tell the user:
 | Fetch existing inline threads (30 threads / 1200 chars) | Same jq |
 | Text after `/review` = review focus | Free text after the PR URL → `--focus` 
|
 | Single-file ledger with sections | A `ledger/` directory, one file per owner 
|
-| Main risk scan → 1-3 full-review subagents + risk-focused → merge → ≤3 
rounds | Identical |
+| Main risk scan → 1-3 full-review subagents + risk-focused → merge → ≤3 
rounds | Same shape, three local additions: every risk item carries a premise 
check the main agent runs before dispatch (4.1); round 1 goes out in two waves 
so the ledger can deduplicate (5.1); a round converges on **severity plus 
coverage**, not on "no new candidates at all" (7) |
+| CI reviews one push in isolation | Earlier runs of the same PR are loaded 
from a stable state directory and read as input (2.1); documents are named by 
head sha and never overwritten (9) |
 | `gh pr review` / Reviews API posting inline comments | **Two `review-docs/` 
documents (EN + ZH) with `path:line` anchors** |
 | CI's review verdict is visible on the PR itself | On a pass, one 
`doris-repo-review/v1` comment from the local account (commit sha, timestamp, 
model, findings, notes); on REQUEST_CHANGES, nothing - the documents stay local 
|
 | 60-minute timeout | No hard timeout, but likewise do not let one round turn 
into unbounded digging |
@@ -402,6 +582,23 @@ Tell the user:
 
 ## 13. Common traps
 
+- **Treating "another Minor appeared" as non-convergence.** A PR of any size 
yields another Minor
+  for as long as you keep looking. What has to stop moving is the *verdict* - 
see step 7. Two runs
+  in a row reporting "did not converge" while the blocking findings were 
settled in round 1 is a
+  broken criterion, not a deep PR.
+- **Dispatching a subagent on a premise you never checked.** The premise check 
of step 4.1 costs
+  one command; skipping it costs a whole agent, and the agent comes back 
having proved you wrong
+  rather than having reviewed anything.
+- **Believing the round-1 ledger deduplicates anything.** It is empty when 
round 1 starts. Use the
+  two waves of step 5.1, or pre-seed it.
+- **Overwriting the previous run's documents.** They are untracked, so an 
overwrite is permanent,
+  and it destroys the only record of what was already considered and 
dismissed. Write
+  `pr-<N>-review.<head7>.*` and let the symlinks move.
+- **Comparing runs by commit hash after a rebase.** A re-reviewed branch is 
often rebased, so
+  `git log old..new` lists the whole PR again and tells you nothing. Compare 
content with
+  `git show <old head>:<path>` before claiming a finding is newly introduced.
+- **Staying inside the diff when the evidence is in a dependency.** See ground 
rule 4a. If a
+  finding's mechanism ends in "…and the library does X", open the library.
 - **The local checkout is ahead of the PR head.** `commit check: ahead:N` 
means there are unpushed
   commits and **they are not part of the review**. This is the easiest thing 
for a reader to
   misread, so state it both in the document header and in the closing report.
diff --git a/skills/doris-repo-review/references/doc-templates.md 
b/skills/doris-repo-review/references/doc-templates.md
index c1a1c1d..17b0c0f 100644
--- a/skills/doris-repo-review/references/doc-templates.md
+++ b/skills/doris-repo-review/references/doc-templates.md
@@ -1,11 +1,16 @@
 # Review document templates
 
-Two documents, same content, same finding IDs, same order:
+Two documents, same content, same finding IDs, same order, named by the head 
that was reviewed:
 
-- `review-docs/pr-<N>-review.en.md`
-- `review-docs/pr-<N>-review.zh.md`
+- `review-docs/pr-<N>-review.<head7>.en.md`
+- `review-docs/pr-<N>-review.<head7>.zh.md`
+- `review-docs/pr-<N>-review.en.md` / `.zh.md` — symlinks to the newest pair
 
-When the branch has no PR, use `review-docs/<branch-slug>-review.en.md` / 
`.zh.md`.
+When the branch has no PR, use 
`review-docs/<branch-slug>-review.<head7>.en.md` / `.zh.md`.
+
+**Never overwrite an earlier run's pair.** `review-docs/` is untracked, so 
overwriting is
+permanent, and what it destroys is the only record of what an earlier review 
already considered
+and dismissed. Re-running against the *same* head overwrites that head's pair, 
and only that one.
 
 The ZH document is a real Chinese review, not a machine translation of the EN 
one: same facts,
 same anchors, same IDs, but idiomatic Chinese. Identifiers, file paths, log 
messages, config
@@ -33,6 +38,13 @@ Local review has no inline comments, so an anchor is the 
only way a reader finds
 | `Blocker` or `Major` | `REQUEST_CHANGES` |
 | only `Minor` / `Nit` / none | `APPROVE` |
 
+The verdict is about severity; **convergence is a separate statement and must 
not be confused with
+it.** A round converges when it produced no new `Blocker`/`Major` *and* the 
coverage report is
+clean (SKILL.md step 7). A run that ended with Minor and Nit findings still 
arriving **converged**
+— report it as such, and name the round after which the verdict stopped 
moving. Only report "did
+not converge" when blocking findings were still appearing at the cap, or when 
changed files
+remained unread; say which of the two it was.
+
 Severity meanings: `Blocker` = correctness, data loss, deadlock, crash, 
incompatible change
 without a compat path, or a broken build/test contract. `Major` = real defect 
or a missing
 guarantee that will bite in production or during upgrade. `Minor` = worth 
fixing, not urgent.
@@ -54,7 +66,8 @@ guarantee that will bite in production or during upgrade. 
`Minor` = worth fixing
 | Reviewed in | `<WORKDIR>` (<ALIGN_MODE>), on <YYYY-MM-DD> |
 | Local alignment | branch check: <…>; commit check: <same / ahead:N / 
behind:N / diverged:a/b> |
 | Verdict | **REQUEST_CHANGES** / **APPROVE** |
-| Rounds | <r> of max 3, converged / not converged |
+| Rounds | <r> of max 3; converged (verdict stable since round <n>) / did not 
converge (<which condition failed>) |
+| Builds on | <prior heads reviewed, from prior_runs/, or "first review of 
this PR"> |
 
 <!-- Only when commit check was ahead:N before aligning: -->
 > `<N>` local commit(s) on `<branch>` are ahead of the PR head and are **not** 
 > covered by
@@ -134,6 +147,9 @@ out. This is what makes the review auditable — do not 
silently drop a concern.
 - Files skimmed and why: <...>
 - Subagents: <round/id/coverage table>
 - Not verified locally: <builds, tests, and anything that needs a running 
cluster>
+- Evidence taken from outside the repository: <dependency artifacts opened, 
with versions, or "none">
+- Inherited from earlier runs: <heads, and how many dismissals were carried 
forward — or "first review of this PR">
+- Convergence: <converged, verdict stable since round N / did not converge 
because <condition>>
 - Uncommitted worktree paths excluded from this review: <from 
worktree_status.txt, or "none">
 ```
 
@@ -153,7 +169,8 @@ out. This is what makes the review auditable — do not 
silently drop a concern.
 | 评审位置 | `<WORKDIR>`(<ALIGN_MODE>),<YYYY-MM-DD> |
 | 本地一致性 | 分支:<…>;commit:<一致 / 领先 N / 落后 N / 已分叉 a/b> |
 | 结论 | **REQUEST_CHANGES** / **APPROVE** |
-| 轮次 | 共 <r> 轮(上限 3),已收敛 / 未收敛 |
+| 轮次 | 共 <r> 轮(上限 3);已收敛(结论自第 <n> 轮起稳定)/ 未收敛(哪个条件没满足) |
+| 承接自 | <prior_runs/ 里更早评审过的 head,若没有则写"本 PR 的首次评审"> |
 
 <!-- 仅当对齐前 commit check 是领先 N 时保留: -->
 > `<branch>` 上有 `<N>` 个本地提交领先于 PR head,**不在**本次评审范围内。
@@ -223,5 +240,8 @@ out. This is what makes the review auditable — do not 
silently drop a concern.
 - 只扫了一眼的文件及原因:<...>
 - 子 agent 分工:<轮次 / id / 覆盖面 表格>
 - 本地未验证:<编译、用例,以及任何需要真实集群才能确认的部分>
+- 仓库之外取到的证据:<打开过的依赖产物及版本,没有就写“无”>
+- 承接自更早的评审:<更早的 head,以及沿用了多少条已排除结论——首次评审就写“本 PR 的首次评审”>
+- 收敛情况:<已收敛,结论自第 N 轮起稳定 / 未收敛,因为 <哪个条件>>
 - 未纳入本次评审的未提交改动:<取自 worktree_status.txt,没有就写“无”>
 ```
diff --git a/skills/doris-repo-review/references/prompts.md 
b/skills/doris-repo-review/references/prompts.md
index 4c15f6a..d3ae4b8 100644
--- a/skills/doris-repo-review/references/prompts.md
+++ b/skills/doris-repo-review/references/prompts.md
@@ -15,6 +15,7 @@ Substitute before use:
 | `{ROUND}` | 1-based round number |
 | `{AGENT_ID}` | short slug, e.g. `r1-fe-spi` |
 | `{FOCUS}` | this subagent's assigned coverage |
+| `{TECHNIQUE}` | the section-D technique this agent must apply, named and 
aimed |
 
 ---
 
@@ -36,6 +37,8 @@ Authoritative PR context (do not obtain the diff or the 
changed-path list any ot
 - User review focus              : {CTX}/review_focus.txt
 - Required AGENTS.md files       : {CTX}/required_agents.txt
 - Shared review ledger directory : {CTX}/ledger/
+- Earlier reviews of this PR     : {CTX}/prior_runs/   (may be absent - then 
this is the first)
+- Coverage checklist             : {CTX}/coverage_checklist.tsv
 - Diff range                     : {BASE_SHA}...{HEAD_SHA} (three-dot, from 
the merge base)
 
 These were generated with `git diff {BASE_SHA}...{HEAD_SHA}` in this worktree. 
The base SHA
@@ -75,6 +78,19 @@ Line-number rule (this run has no GitHub inline comments, so 
anchors are the onl
 Evidence rule: for any claimed error you must give the concrete path or logic 
where it occurs.
 "If A then B" is only acceptable when you name a concrete scenario in which A 
actually happens.
 
+Evidence may live outside the repository, and you are expected to go and get 
it. The change list
+comes only from the files above - but evidence does not. When a dependency's 
own content is what
+decides the behaviour under review, open that dependency: unzip the jar under 
`~/.m2/repository`
+that the build actually pins and read the resource inside it, read its 
`-sources.jar`, read the
+vendored service definition, read the JDK class whose contract a comment 
claims. Name the artifact
+and its version in the finding so the evidence is reproducible. A review that 
never leaves the diff
+cannot find a defect whose two halves are "the changed line" and "what the 
changed line now
+reaches".
+
+If `{CTX}/prior_runs/` exists it holds what earlier reviews of this same PR 
concluded. Read it with
+`pr_review_threads.md`: a dismissal recorded there with evidence does not need 
re-deriving, and an
+accepted finding recorded there is something the author has already been told.
+
 Do NOT stop after finding the first blocking issue. Keep reviewing changed 
files, related
 control flow, tests, and parallel or special-case paths until all plausible 
correctness,
 lifecycle, configuration, compatibility, performance, and coverage bugs have 
been investigated
@@ -109,6 +125,10 @@ coverage. For optimizer/Nereids changes, follow the 
plan-tree output style in Pa
 
 Read the actual surrounding code, not only the diff hunks: a change is only 
correct with
 respect to its real call chain, its concurrency, and its lifecycle.
+
+Technique to apply: {TECHNIQUE}
+See section D of this file. Use it deliberately rather than falling back on 
"read the diff and
+look for mistakes" - that finds what a careful author already found.
 ```
 
 ---
@@ -139,4 +159,60 @@ until you can answer the question with concrete code 
evidence. Then record in yo
 Report a candidate finding only when the evidence is concrete. "Not a bug" is 
a valid and
 useful answer, but it must come with the code evidence that rules the concern 
out — the main
 agent records that evidence as a dismissal.
+
+Technique to apply: {TECHNIQUE}
+
+Premise: {PREMISE} — status: {PREMISE_RESULT}
+If that status is "cannot be checked cheaply", check the premise FIRST and 
stop early if it is
+false: say so, with the command that shows it, and do not spend the rest of 
your budget.
 ```
+
+---
+
+## D. Technique catalogue (name one in every prompt)
+
+A subagent told only "review your slice" invents a method, and the method it 
invents is usually
+"read the diff and look for mistakes" - which finds what a careful author 
already found. The
+techniques below are the ones that actually produced findings. Name the one 
you want, and say what
+it should be applied to.
+
+**D1. Differential against the base.** *Do not read the diff. Reconstruct both 
sides.* Extract the
+pre-change files with `git show {BASE_SHA}:<path>`, build the old and the new 
behaviour tables
+yourself - one row per (input, condition) the code distinguishes - and list 
every cell that
+differs. Then classify each differing cell as intended (name the commit that 
says so) or as a
+regression. This is what catches a defect whose changed line is *correct*: the 
line is right, the
+comment explaining it is right, and the consequence two modules away is wrong.
+
+**D2. What did this switch turn on?** When a change fixes something that was 
silently not working,
+the path it revives has never been exercised. Ask: what else is on that path, 
what has never run,
+what does the newly-live path now hand to a consumer that was never built to 
receive it?
+
+**D3. Contract-boundary and dialect check.** At every boundary where one 
component hands text,
+names, or payloads to another - SPI, plugin, RPC, SQL text, config - ask who 
owns the format, who
+validates it, and what happens to a value that parses but means something 
different on the other
+side. Silent widening lives here.
+
+**D4. Doc versus code.** Read the PR body, the release note, the module 
README, the AGENTS.md
+obligations and the javadoc as *claims*, and check each against the code. A 
claim that the code
+contradicts is a finding even when the code is right, because the claim is 
what the next person
+acts on. Machine-checked obligations that a comment says are review-only are 
your job by definition.
+
+**D5. Parallel paths.** For every fix, find its siblings: the other call sites 
of the same shape,
+the other plugin family, the other branch of the same method, the other error 
code with the same
+arity. A fix applied to one of N identical sites is N-1 findings.
+
+**D6. Failure-mode enumeration.** For each new interface method, enumerate 
`{null, empty,
+exception, wrong type}` as the answer and trace what the engine does with each 
on every consuming
+path. State for each whether the outcome is a correct refusal, a hard error, 
or a silent grant.
+
+**D7. Coverage-of-the-gate.** For every test that is claimed to be a gate, ask 
what change would
+leave it green. A frozen baseline that records less than the obligation it 
enforces, an assertion
+that restates what the code computes, a mock that stubs out the very default 
under test, a negative
+control that is not negative.
+
+**D8. Completeness critic (round 3, or whenever a round disappoints).** Not a 
slice. Build the
+coverage map from the ledger, list the changed files nobody demonstrably 
opened, read them. Then
+find the two or three most load-bearing "not a bug" conclusions in the ledger 
and re-derive them
+from primary sources. Then ask which *kinds* of check were never run at all - 
persistence and
+replay, upgrade *and downgrade*, CI gates, licence obligations, the thrift 
surface - and run the
+ones that apply.
diff --git a/skills/doris-repo-review/scripts/coverage-report.sh 
b/skills/doris-repo-review/scripts/coverage-report.sh
new file mode 100755
index 0000000..f68f396
--- /dev/null
+++ b/skills/doris-repo-review/scripts/coverage-report.sh
@@ -0,0 +1,115 @@
+#!/usr/bin/env bash
+# Mechanical coverage report for the `doris-repo-review` skill (step 6a).
+#
+# Answers one question, with grep rather than judgement: which of this PR's
+# changed files has no ledger file so much as mentioned yet?
+#
+# A file nobody read is not a file with no findings. Running this after every
+# round is what lets the next round be aimed instead of guessed, and what keeps
+# "13 changed files nobody opened" from being a round-3 discovery.
+#
+# Usage:
+#   coverage-report.sh --ctx <dir> [--round <n>] [--quiet]
+#
+# Reads  : <ctx>/pr_changed_files.txt, <ctx>/ledger/*.md
+# Writes : <ctx>/coverage_report.txt  (and appends one line to 
<ctx>/coverage_history.tsv)
+# Exit   : 0 when every changed file is mentioned, 1 when some are not.
+set -euo pipefail
+
+CTX=""
+ROUND=""
+QUIET=0
+while [ $# -gt 0 ]; do
+    case "$1" in
+        --ctx)   CTX="$2"; shift 2 ;;
+        --round) ROUND="$2"; shift 2 ;;
+        --quiet) QUIET=1; shift ;;
+        -h|--help)
+            sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'
+            exit 0 ;;
+        *) echo "unknown argument: $1" >&2; exit 2 ;;
+    esac
+done
+
+[ -n "$CTX" ] || { echo "ERROR: --ctx is required" >&2; exit 2; }
+[ -d "$CTX" ] || { echo "ERROR: no such context directory: $CTX" >&2; exit 2; }
+CHANGED="$CTX/pr_changed_files.txt"
+[ -f "$CHANGED" ] || { echo "ERROR: $CHANGED not found - run 
prepare-review-context.sh first" >&2; exit 2; }
+
+python3 - "$CTX" "${ROUND:-}" "$QUIET" <<'PY'
+import os, sys, glob, datetime
+
+ctx, round_label, quiet = sys.argv[1], sys.argv[2], sys.argv[3] == "1"
+changed = [l.strip() for l in open(os.path.join(ctx, "pr_changed_files.txt")) 
if l.strip()]
+
+ledger_files = sorted(
+    p for p in glob.glob(os.path.join(ctx, "ledger", "*.md"))
+    # main-merged is the main agent's own summary; the risk scan is written
+    # before anyone reads anything. Neither is evidence that a file was read.
+    if os.path.basename(p) not in ("README.md", "main-merged.md", 
"00-main-risk-scan.md")
+)
+
+blobs = {}
+for p in ledger_files:
+    try:
+        blobs[os.path.basename(p)] = open(p, errors="replace").read()
+    except OSError:
+        pass
+
+def mentions(path, text):
+    """A file counts as mentioned when its full path or its basename 
appears."""
+    if path in text:
+        return True
+    base = os.path.basename(path)
+    # A bare basename is weak evidence for a common name (pom.xml, README.md),
+    # so require the full path for those.
+    return len(base) > 12 and base in text
+
+unread, by_owner = [], {}
+for path in changed:
+    owners = [name for name, text in blobs.items() if mentions(path, text)]
+    if owners:
+        for o in owners:
+            by_owner.setdefault(o, 0)
+            by_owner[o] += 1
+    else:
+        unread.append(path)
+
+lines = []
+lines.append("=== coverage report ===")
+lines.append(f"round          : {round_label or '(unspecified)'}")
+lines.append(f"changed files  : {len(changed)}")
+lines.append(f"mentioned      : {len(changed) - len(unread)}")
+lines.append(f"NOT mentioned  : {len(unread)}")
+lines.append(f"ledger files   : {len(blobs)}")
+if by_owner:
+    lines.append("")
+    lines.append("files mentioned per ledger owner:")
+    for name in sorted(by_owner):
+        lines.append(f"  {by_owner[name]:4d}  {name}")
+if unread:
+    lines.append("")
+    lines.append("changed files no ledger file mentions yet:")
+    lines.extend("  " + p for p in unread)
+    lines.append("")
+    lines.append("These are not files with no findings - they are files nobody 
has read.")
+    lines.append("Cover them in the next round's slicing, or read them 
yourself in step 8.")
+else:
+    lines.append("")
+    lines.append("Every changed file is mentioned by at least one ledger 
file.")
+
+report = "\n".join(lines) + "\n"
+open(os.path.join(ctx, "coverage_report.txt"), "w").write(report)
+
+hist = os.path.join(ctx, "coverage_history.tsv")
+if not os.path.exists(hist):
+    open(hist, "w").write("date\tround\tchanged\tmentioned\tunread\n")
+with open(hist, "a") as f:
+    f.write("%s\t%s\t%d\t%d\t%d\n" % (
+        datetime.date.today().isoformat(), round_label or "-",
+        len(changed), len(changed) - len(unread), len(unread)))
+
+if not quiet:
+    sys.stdout.write(report)
+sys.exit(1 if unread else 0)
+PY
diff --git a/skills/doris-repo-review/scripts/prepare-review-context.sh 
b/skills/doris-repo-review/scripts/prepare-review-context.sh
index e37d764..b75ff36 100755
--- a/skills/doris-repo-review/scripts/prepare-review-context.sh
+++ b/skills/doris-repo-review/scripts/prepare-review-context.sh
@@ -166,6 +166,12 @@ GIT_DIFF=(git -c diff.renameLimit=32768 diff --no-ext-diff 
--no-color)
 
 "${GIT_DIFF[@]}" "$DIFF_RANGE"                                  > 
"$CTX/pr.diff"
 "${GIT_DIFF[@]}" --name-only "$DIFF_RANGE"                      > 
"$CTX/pr_changed_files.txt"
+# One row per changed file, for the mechanical coverage report of step 6a.
+# A file nobody read is not a file with no findings.
+{
+    printf 'path\tstatus\n'
+    "${GIT_DIFF[@]}" --name-status "$DIFF_RANGE" | awk -F'\t' 'NF>=2 {print 
$NF "\t" $1}'
+} > "$CTX/coverage_checklist.tsv"
 "${GIT_DIFF[@]}" --name-status -M "$DIFF_RANGE"                 > 
"$CTX/pr_changed_files_status.txt"
 "${GIT_DIFF[@]}" --stat "$DIFF_RANGE"                           > 
"$CTX/pr_diffstat.txt"
 git log --oneline --no-decorate "${MERGE_BASE}..${HEAD_SHA}"    > 
"$CTX/pr_commits.txt"
@@ -399,6 +405,32 @@ fi
 
 # ------------------------------------------------------------------ meta + 
report
 [ -n "$DOCS_ROOT" ] || DOCS_ROOT="$REPO_ROOT"
+# --------------------------------------------------- prior runs of this same 
PR
+# A PR is normally reviewed more than once. Each run used to start from zero 
and
+# leave nothing behind, so the expensive half of a review - the dismissals, 
with
+# their evidence - was re-derived every time, and "was this considered last
+# time?" was unanswerable. Runs are kept under a stable per-PR directory that 
is
+# outside both the session scratchpad and the git worktree.
+STATE_ROOT="${DORIS_REVIEW_STATE:-${XDG_CACHE_HOME:-$HOME/.cache}/doris-repo-review}"
+STATE_DIR=""
+PRIOR_RUNS=0
+if [ -n "${PR_NUMBER:-}" ]; then
+    STATE_DIR="${STATE_ROOT}/$(printf '%s' "${UPSTREAM_REPO}" | tr '/' 
'-')/pr-${PR_NUMBER}"
+    mkdir -p "$STATE_DIR/runs"
+    mkdir -p "$CTX/prior_runs"
+    for run in "$STATE_DIR"/runs/*/; do
+        [ -d "$run" ] || continue
+        head7="$(basename "$run")"
+        # The run for this very head is this run, not a prior one.
+        [ "$head7" = "${HEAD_SHA:0:7}" ] && continue
+        [ -f "$run/main-merged.md" ] || continue
+        cp "$run/main-merged.md" "$CTX/prior_runs/${head7}-main-merged.md"
+        [ -f "$run/meta.env" ] && cp "$run/meta.env" 
"$CTX/prior_runs/${head7}-meta.env"
+        PRIOR_RUNS=$((PRIOR_RUNS + 1))
+    done
+    [ -f "$STATE_DIR/index.tsv" ] && cp "$STATE_DIR/index.tsv" 
"$CTX/prior_runs/index.tsv"
+fi
+
 {
     echo "REPO_ROOT=$REPO_ROOT"
     echo "DOCS_ROOT=$DOCS_ROOT"
@@ -417,6 +449,8 @@ fi
     echo "MERGE_BASE=$MERGE_BASE"
     echo "DIFF_RANGE=$DIFF_RANGE"
     echo "DIRTY_FILES=$DIRTY_COUNT"
+    echo "STATE_DIR=${STATE_DIR:-}"
+    echo "PRIOR_RUNS=${PRIOR_RUNS:-0}"
     echo "REVIEW_DATE=$(date +%Y-%m-%d)"
 } > "$CTX/meta.env"
 
@@ -428,6 +462,14 @@ echo "commits       : $(grep -c . "$CTX/pr_commits.txt" || 
true)"
 echo "diff lines    : $(wc -l < "$CTX/pr.diff" | tr -d ' ')"
 echo "diffstat      : $(tail -n 1 "$CTX/pr_diffstat.txt")"
 echo
+if [ "${PRIOR_RUNS:-0}" -gt 0 ]; then
+    echo "prior runs of this PR (read them, see SKILL.md 2.1): $PRIOR_RUNS"
+    ls -1 "$CTX/prior_runs" | sed 's/^/  /'
+    echo
+else
+    echo "prior runs of this PR: none - this is the first review"
+    echo
+fi
 echo "required AGENTS.md:"
 sed 's/^/  /' "$CTX/required_agents.txt"
 if [ "${DIRTY_COUNT:-0}" -gt 0 ]; then
diff --git a/skills/doris-repo-review/scripts/save-run-state.sh 
b/skills/doris-repo-review/scripts/save-run-state.sh
new file mode 100755
index 0000000..d9294e8
--- /dev/null
+++ b/skills/doris-repo-review/scripts/save-run-state.sh
@@ -0,0 +1,86 @@
+#!/usr/bin/env bash
+# Persist what this run concluded, so the next review of the same PR starts 
from
+# it instead of from zero (`doris-repo-review` skill, step 11).
+#
+# What gets kept is the merged ledger - and the expensive part of that is the
+# "Considered and Dismissed" set, with its evidence. A later run reads it back
+# through <ctx>/prior_runs/ and carries the dismissals forward rather than
+# re-deriving them, which is also what makes "was this considered last time?"
+# an answerable question.
+#
+# Usage:
+#   save-run-state.sh --ctx <dir> --verdict <APPROVE|REQUEST_CHANGES> \
+#                     --findings <blocker>,<major>,<minor>,<nit> \
+#                     --rounds <n> --converged <true|false> \
+#                     [--docs <path>[,<path>]] [--note <text>]
+#
+# Writes <state>/runs/<head7>/{main-merged.md,meta.env} and appends 
<state>/index.tsv,
+# where <state> is the STATE_DIR recorded in <ctx>/meta.env by 
prepare-review-context.sh.
+set -euo pipefail
+
+CTX=""; VERDICT=""; FINDINGS=""; ROUNDS=""; CONVERGED=""; DOCS=""; NOTE=""
+while [ $# -gt 0 ]; do
+    case "$1" in
+        --ctx)       CTX="$2"; shift 2 ;;
+        --verdict)   VERDICT="$2"; shift 2 ;;
+        --findings)  FINDINGS="$2"; shift 2 ;;
+        --rounds)    ROUNDS="$2"; shift 2 ;;
+        --converged) CONVERGED="$2"; shift 2 ;;
+        --docs)      DOCS="$2"; shift 2 ;;
+        --note)      NOTE="$2"; shift 2 ;;
+        -h|--help)   sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
+        *) echo "unknown argument: $1" >&2; exit 2 ;;
+    esac
+done
+
+[ -n "$CTX" ] || { echo "ERROR: --ctx is required" >&2; exit 2; }
+[ -f "$CTX/meta.env" ] || { echo "ERROR: $CTX/meta.env not found" >&2; exit 2; 
}
+
+read_meta() { sed -n "s/^$1=//p" "$CTX/meta.env" | head -n 1; }
+STATE_DIR="$(read_meta STATE_DIR)"
+HEAD_SHA="$(read_meta HEAD_SHA)"
+PR_NUMBER="$(read_meta PR_NUMBER)"
+REVIEW_DATE="$(read_meta REVIEW_DATE)"
+
+if [ -z "$STATE_DIR" ]; then
+    echo "No STATE_DIR in meta.env (the PR number was not resolved), so this 
run cannot be saved." >&2
+    echo "That is not fatal - it only means the next review starts without 
this one's dismissals." >&2
+    exit 0
+fi
+[ -n "$VERDICT" ]   || { echo "ERROR: --verdict is required" >&2; exit 2; }
+[ -n "$FINDINGS" ]  || { echo "ERROR: --findings is required" >&2; exit 2; }
+[ -n "$ROUNDS" ]    || { echo "ERROR: --rounds is required" >&2; exit 2; }
+[ -n "$CONVERGED" ] || { echo "ERROR: --converged is required" >&2; exit 2; }
+case "$VERDICT" in APPROVE|REQUEST_CHANGES) ;; *) echo "ERROR: bad --verdict: 
$VERDICT" >&2; exit 2 ;; esac
+case "$CONVERGED" in true|false) ;; *) echo "ERROR: --converged must be true 
or false" >&2; exit 2 ;; esac
+echo "$FINDINGS" | grep -Eq '^[0-9]+,[0-9]+,[0-9]+,[0-9]+$' \
+    || { echo "ERROR: --findings must be <blocker>,<major>,<minor>,<nit>" >&2; 
exit 2; }
+
+HEAD7="${HEAD_SHA:0:7}"
+RUN_DIR="$STATE_DIR/runs/$HEAD7"
+mkdir -p "$RUN_DIR"
+
+if [ -f "$CTX/ledger/main-merged.md" ]; then
+    cp "$CTX/ledger/main-merged.md" "$RUN_DIR/main-merged.md"
+else
+    echo "WARNING: no $CTX/ledger/main-merged.md to save - the next run 
inherits nothing." >&2
+fi
+cp "$CTX/meta.env" "$RUN_DIR/meta.env"
+[ -f "$CTX/coverage_report.txt" ] && cp "$CTX/coverage_report.txt" 
"$RUN_DIR/coverage_report.txt"
+
+INDEX="$STATE_DIR/index.tsv"
+[ -f "$INDEX" ] || printf 
'date\thead\tverdict\tblocker,major,minor,nit\trounds\tconverged\tdocs\tnote\n' 
> "$INDEX"
+# One row per head. Re-running against an unchanged head replaces its row.
+if grep -q "   ${HEAD7}        " "$INDEX" 2>/dev/null; then
+    grep -v "  ${HEAD7}        " "$INDEX" > "$INDEX.tmp" && mv "$INDEX.tmp" 
"$INDEX"
+fi
+printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
+    "${REVIEW_DATE:-$(date +%Y-%m-%d)}" "$HEAD7" "$VERDICT" "$FINDINGS" \
+    "$ROUNDS" "$CONVERGED" "${DOCS:-}" "${NOTE:-}" >> "$INDEX"
+
+echo "saved run state for PR #${PR_NUMBER:-?} head $HEAD7"
+echo "  $RUN_DIR"
+echo "  $INDEX"
+echo
+echo "history for this PR:"
+column -t -s ' ' "$INDEX" 2>/dev/null || cat "$INDEX"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to