| name | re-review-state |
| description | Use when re-running a review on a PR that already has prior comments; computes stable finding-keys and decides per finding whether to do nothing, reply-fixed and resolve, escalate as improperly resolved, escalate as a disagreement, or post an incorrect-fix follow-up. |
Skill: Re-review state (phases A–D, finding-keys, decision table)
This skill is the procedural mechanics of how a reviewer agent
behaves on the second-and-later run on the same PR. It is referenced
by every reviewer agent and by the aggregator.
The behavioral rules and the decision table live in the review
contract §7. This skill walks through the algorithm in detail.
1. Phase A — Inventory prior comments
1a. Fetch the agent's prior inline comments
GET /repos/{owner}/{repo}/pulls/{pull_number}/comments?per_page=100
Authorization: Bearer ${TOKEN}
Accept: application/vnd.github+json
Paginate via the Link header until exhausted. For each comment,
parse the trailing HTML footer:
<!-- fprime-agent: <agent-name>; finding-key: <hex>; site-key: <hex>; v2[; reply-kind: <kind>] -->
(Legacy v1 footers omit site-key; parse both forms.)
Keep all agent-authored comments (any fprime-agent: footer):
- Comments where
<agent-name> matches <self> are the agent's
prior comments on this PR (drive phases C–D as before).
- ALL agent-authored comments — own and others' — are additionally
indexed by
site-key for the cross-agent concurrence check
(review contract §6a). For v1 comments without a site-key,
recompute it best-effort from the comment's path and anchor
context.
1b. Fetch each thread's resolution status and reply chain
Use the GraphQL query in post-inline-review §5. For each
prior comment, retrieve:
thread.id (needed for resolveReviewThread /
unresolveReviewThread).
thread.isResolved (drives the improperly-resolved case).
thread.resolvedBy.login (recorded for audit; not used in the
decision).
- The
thread.comments[] list (drives disagreement detection: the
agent looks for any comment authored by a user other than itself).
1c. Index by finding-key and site-key
Build a dictionary keyed by finding-key (own comments only) whose
value is { comment_id, thread_id, path, line, is_resolved, has_contributor_replies, has_prior_disagreement_reply }.
has_prior_disagreement_reply is true iff a comment in the thread
carries reply-kind: disagreement in its HTML footer.
Build a second dictionary keyed by site-key (ALL agent-authored
comments) whose value is a list of { agent_name, comment_id, thread_id, path, is_resolved, tag, body, has_own_concurrence_reply, closed_as_duplicate }.
has_own_concurrence_reply is true iff the thread carries a
reply-kind: concurrence reply from <self>. closed_as_duplicate
is true iff the thread carries a reply-kind: duplicate-close reply
from the aggregator (fprime-review-summary footer); such threads
are not concurrence targets — follow the link to the canonical
thread instead.
2. Phase B — Run scope checker on the new head
Run the agent's analysis on the PR head SHA. Produce the
current_findings set: a list of { finding-key, path, line, tag, body } tuples.
The finding-key is computed identically to phase A:
finding-key = sha256(
agent_name + "|" +
file_path + "|" +
anchor + "|" +
finding_class
)
The agent-agnostic site-key (review contract §6a) reuses the same
inputs minus the agent and class:
site-key = sha256(file_path + "|" + anchor)
Where:
agent_name is the agent's short name from the registry.
file_path is the repo-root-relative path of the offending line.
anchor is not a line number. Computation:
- Resolve the enclosing symbol name at the offending line —
the deepest containing function / method / class / FPP entity.
Use the language-appropriate tooling (
ctags, clang -ast-dump,
fpp-aware parser for .fpp files). For .md / .yml files,
the section heading or top-level YAML key serves as the symbol.
- Compute a 40-character line fingerprint: the offending
line, stripped of leading/trailing whitespace and collapsed
interior whitespace, truncated or padded to exactly 40
characters with right-space padding.
anchor = enclosing_symbol + ":" + fingerprint.
- If the symbol cannot be resolved (rare), fall back to
anchor = "<file>:" + fingerprint. The fingerprint alone is
still stable across line drift.
finding_class is the agent-defined name of the finding (e.g.,
ground-reachable-assert, cmd-arg-overflow, unverified-action).
Each agent's .agent.md enumerates its finding classes.
The same finding (same symbol, same line content, same class) will
produce the same finding-key across reformatting and line drift,
which is the whole point.
3. Phase C — Match and act
The full decision table is in the review contract §7. Operationally:
prior_keys = { k : k in agent.prior_comments_index }
current_keys = { k : k in current_findings }
intersect = prior_keys ∩ current_keys
resolved = prior_keys − current_keys
new = current_keys − prior_keys
3a. Threads where prior and current both present (intersect)
For each k in intersect, decide which row of the table applies:
thread.isResolved | has_contributor_replies AND NOT has_prior_disagreement_reply | Action |
|---|
true | — | Improperly resolved. Un-resolve + reply (see §3a-i). |
false | true | Disagreement escalation. Reply once + maintainer ping (see §3a-ii). |
false | false | Do nothing. Leave the comment as-is. Never repost. |
3a-i. Improper-resolution action
- Call
unresolveReviewThread(input: { threadId }) (GraphQL).
- POST a reply on the thread with the improper-resolution body
shape from review contract §9.
- Append a maintainer ping per
maintainer-lookup. The
improper-resolution case ALWAYS pings the maintainer (not gated
on confidence) because the contributor's resolve action conflicts
with the agent's re-analysis.
- Increment
improperly resolved in the Since-last-run counter.
- Do NOT decrement any tag column. Do NOT decrement
outstanding.
If unresolveReviewThread fails with a permissions error, post the
reply anyway and increment the counter. The thread remains visibly
resolved but the reply + maintainer ping is visible inline.
3a-ii. Disagreement-escalation action
- POST a reply on the thread with the disagreement-escalation body
shape from review contract §9.
- Append a maintainer ping per
maintainer-lookup. Always
pings (the whole point is human adjudication).
- Do NOT resolve the thread. Leave it open for the maintainer.
- Do NOT repost the original finding.
- Increment
disagreements escalated in the Since-last-run
counter.
3b. Threads where prior present but current absent (resolved)
For each k in resolved:
thread.isResolved | Action |
|---|
false | Clean resolution. Reply [<review_label>] Fixed in <head-sha>. + GraphQL resolveReviewThread. |
true | Acknowledged. Reply [<review_label>] Fixed in <head-sha>. only — no need to re-resolve. |
Increment resolved in Since-last-run. Decrement outstanding (do
NOT decrement any tag column).
3c. Current findings with no prior match (new)
For each k in new:
-
If there exists a prior finding p such that:
p.path == new.path
p.enclosing_symbol == new.enclosing_symbol
p.finding_class != new.finding_class
This is an incorrect-fix follow-up: the contributor attempted
a fix that resolved the original finding but left a different one
in the same spot. POST a new inline comment whose body begins:
[<review_label>] **<tag>** Follow-up to <link to prior comment>: <new issue>
Then follow the normal fresh-finding body shape from review
contract §9.
Increment incorrect-fix follow-ups in Since-last-run AND
newly added (an incorrect-fix follow-up is also a newly-added
comment).
-
Otherwise, run the cross-agent concurrence check (review
contract §6a): look up new.site-key in the all-agents site-key
index. If another agent has an OPEN thread (not resolved, not
closed_as_duplicate) at the same site-key describing the same
underlying issue:
- Do NOT open a new thread. POST one concurrence reply on that
thread per the concurrence body shape (review contract §9),
unless
has_own_concurrence_reply is already true (one
concurrence per agent per thread).
- Still count the finding in the agent's own metadata (tag column,
outstanding, newly added) at the agent's own severity.
- On later runs, the agent's Phase C tracks this finding on the
shared thread (resolve semantics per review contract §6a).
If the site-key matches but the issue is genuinely different, post
normally — the site-key alone never suppresses a distinct finding.
-
Otherwise: brand-new finding. POST a new inline comment per
the fresh-finding body shape. Increment newly added in
Since-last-run.
4. Phase D — Update the per-agent metadata review
Dismiss the prior metadata review (located by the HTML marker) via
PUT /repos/{o}/{r}/pulls/{n}/reviews/{id}/dismissals with message
Superseded by re-review run N. Then submit a new review with the
updated metadata body.
Update:
- The four tag columns: increment for any newly-posted comments
(incorrect-fix follow-ups and brand-new findings). Never decrement.
- The
outstanding column: recompute as
(cumulative tag-column sum) − (cumulative resolved count).
- The
Verdict: line: Go iff outstanding must-fix == 0, else
No-Go.
- The
Run: line: increment the run ordinal.
- The
Since last run: line: write the six counters
(resolved, still open, newly added, incorrect-fix follow-ups, improperly resolved, disagreements escalated).
still open = |intersect| (after subtracting improperly-resolved
and disagreement-escalated entries, since those are accounted in
their own counters but still represent the same finding-keys that
"remain open"; in the simple accounting model still open is
|intersect| and the other two counters are subsets reported
separately).
The cumulative tag columns and outstanding-driven verdict are
defined in the review contract §2.
5. Guardrails (do not violate)
- Never repost a finding whose
finding-key matches an existing
comment from the same agent on this PR.
- Never resolve a comment whose
finding-key is still in
current_keys.
- Never silently accept a contributor's resolution of a thread
whose
finding-key is still present. Un-resolve and reply per the
improperly-resolved flow.
- Never argue. On disagreement, the agent posts ONE escalation
reply + maintainer ping. Subsequent runs leave the thread alone
(the de-dup key is the
reply-kind: disagreement HTML attribute).
- Never decrement a tag column. Resolution affects only
outstanding.
- Never open a new thread at a site-key where another agent's
open thread already describes the same issue — concur instead
(review contract §6a).
- Never un-resolve a thread carrying a
reply-kind: duplicate-close reply from the aggregator; the linked canonical
thread is the live home of the finding.
6. Edge cases
6a. The same offending line is re-introduced after a clean resolution
Prior run resolved the finding. A later commit re-introduces the
same offending line in the same symbol. The finding-key is
identical to a prior (resolved) one.
Behavior: the agent posts a new inline comment. The cumulative
tag column increments. outstanding increments. The prior resolved
thread stays resolved (it's a different comment now). Since-last-run
reports the new comment under newly added.
This is suboptimal — ideally the comment would re-open the prior
thread — but GitHub does not support that operation. The agent's
heuristic is: post a fresh comment and prefix the body (after the
reviewer label) with (This issue was previously resolved on <commit-sha> and was re-introduced.)
6b. The contributor rewrites history (force-push)
If the prior commits the agent reviewed are no longer in the PR
history, the GitHub comments are likely orphaned (their line
positions are no longer valid). The agent treats every prior comment
as if its finding-key is absent from current_keys ONLY if
GitHub's response indicates the comment is on a nonexistent commit;
otherwise the comments are still attached to the rewritten history
and the normal flow applies.
6c. The contributor closes the thread without addressing AND replies
Both improperly-resolved and disagreement-escalation conditions are
true. The improperly-resolved row takes precedence (it's the more
specific case). The agent un-resolves, replies with the
improper-resolution shape, and increments improperly resolved. The
escalation handles disagreement-via-resolve and disagreement-via-
reply in one motion; no need to double-post.
6d. The aggregator FAILED and is not on the PR
If the orchestrator reports the aggregator as FAILED, the reviewer
agents still complete their phases A–D normally. The aggregator's
top-level summary is the part missing. The orchestrator's status
message to the human operator covers this case.
6e. The TOKEN lacks unresolveReviewThread permission
Post the improper-resolution reply on the thread anyway. The thread
remains visibly resolved on GitHub but the inline reply +
maintainer ping makes the un-acknowledged finding visible. Increment
improperly resolved regardless.
7. One-line summary
A: index prior comments by finding-key plus thread state. B: re-run analysis, compute current finding-keys. C: decide per row of the contract §7 table — do-nothing, resolve, reply-improper-resolution, reply-disagreement, post-new, post-incorrect-fix-follow-up. D: dismiss prior metadata review, submit new one.