feat: harden PR queue reconciliation against stale/conflicting state (#166)

- Add updated_at (list_prs), + merged_at/merge_commit_sha/closed_at/updated_at (view_pr) for staleness detection in tool output and inventory.
- Surface updated_at in gitea_review_pr inventory report.
- Strengthen gitea-pr-review skill steps, _COMMON_WORKFLOWS, _GUIDE_RULES with explicit live reconciliation checklist, 'do not trust prior handoffs', stop on conflict.
- Add dedicated live queue reconciliation runbook section in docs/llm-workflow-runbooks.md.
- Add passthrough tests and assertions for new fields.
- All gates, redaction, author/reviewer separation untouched.
- Tests pass, py_compile clean, diff clean, secret sweep clean.

Closes #166
This commit is contained in:
2026-07-05 12:45:05 -04:00
parent a02d990bc5
commit 194301359e
4 changed files with 77 additions and 12 deletions
+35 -6
View File
@@ -402,8 +402,9 @@ def gitea_list_prs(
repo: Override the repository name.
Returns:
List of dicts with 'number', 'title', 'state', 'head', 'base', and
'mergeable' ('url' only with the reveal opt-in).
List of dicts with 'number', 'title', 'state', 'head', 'base',
'mergeable', 'updated_at' ('url' only with the reveal opt-in).
The additional 'updated_at' aids stale/conflicting queue detection.
"""
h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h)
@@ -418,6 +419,7 @@ def gitea_list_prs(
"head": pr["head"]["ref"],
"base": pr["base"]["ref"],
"mergeable": pr.get("mergeable"),
"updated_at": pr.get("updated_at"),
}
results.append(_with_optional_url(entry, pr.get("html_url")))
return results
@@ -441,7 +443,10 @@ def gitea_view_pr(
repo: Override the repository name.
Returns:
dict with PR details.
dict with PR details including 'updated_at', 'merged_at',
'merge_commit_sha', 'closed_at' (when present) to support
live-state reconciliation and staleness detection against
prior reports or handoffs.
"""
h, o, r = _resolve(remote, host, org, repo)
auth = _auth(h)
@@ -456,6 +461,10 @@ def gitea_view_pr(
"base": pr["base"]["ref"],
"mergeable": pr.get("mergeable"),
"user": pr.get("user", {}).get("login", ""),
"updated_at": pr.get("updated_at"),
"merged_at": pr.get("merged_at"),
"merge_commit_sha": pr.get("merge_commit_sha"),
"closed_at": pr.get("closed_at"),
}
return _with_optional_url(result, pr.get("html_url"))
@@ -1404,6 +1413,7 @@ def gitea_review_pr(
"linked_issues": list(set(linked)),
"labels": labels,
"head_sha": pr_head_sha,
"updated_at": pr.get("updated_at"),
"requires_non_author_reviewer": req_non_author
})
except Exception as e:
@@ -1429,6 +1439,7 @@ def gitea_review_pr(
f" Author: {item['author']}\n"
f" Base/Head: {item['base']} / {item['head']}\n"
f" Head SHA: {item['head_sha']}\n"
f" Updated: {item.get('updated_at') or 'unknown'}\n"
f" Mergeability: {item['mergeable']}\n"
f" Linked Issues: {item['linked_issues']}\n"
f" Labels: {item['labels']}\n"
@@ -1968,6 +1979,12 @@ _GUIDE_RULES = {
"Do not hardcode Gitea identities; always dynamically verify your "
"identity and resolve task capability via gitea_resolve_task_capability.",
],
"live_state_reconciliation": [
"Never rely on prior handoffs, chat history, or cached reports for PR queue state.",
"Before review or merge decisions: always call gitea_list_prs + gitea_view_pr (for the specific PR), re-fetch remotes, pull master --ff-only, and compare live fields (state, mergeable, head SHA, updated_at, merged_at, merge_commit_sha) against any prior claims.",
"Conflicting or stale PR state (e.g. prior said merged but live open, or head/updated mismatch) is a hard blocker: report it explicitly and stop until live state is unambiguous.",
"After merge, re-list PRs and re-view the PR (plus verify master contains the merge) before claiming completion.",
],
}
_COMMON_WORKFLOWS = [
@@ -1978,10 +1995,15 @@ _COMMON_WORKFLOWS = [
"implementation: claim issue, branch from fresh master, implement only "
"the issue scope, test, open a PR referencing the issue, stop.",
"PR review: verify reviewer identity (must be a reviewer profile/namespace), "
"pin the head SHA, validate independently, post a verdict; never review your own work.",
"reconcile live state first (list_prs + view_pr selected PR immediately; "
"verify state/mergeable/head SHA/updated_at/merged info vs any prior reports; "
"git fetch --prune and pull --ff-only master; check linked issue state); "
"pin the head SHA; treat conflicting/stale reports as blocker and stop; "
"validate independently, post a verdict; never review your own work. "
"After merge, re-list and re-view.",
"PR merge: reviewer identity (must be a reviewer profile/namespace) + "
"eligibility check + pinned head + explicit 'MERGE PR <n>' confirmation, "
"only with operator authorization.",
"only with operator authorization. Reconcile live state before deciding.",
]
# Skill registry (#128). status: 'available' = backed by tools in this
@@ -2042,11 +2064,18 @@ _PROJECT_SKILLS = {
"to confirm reviewer namespace and avoid author-profile blocks.",
"Verify reviewer identity with gitea_whoami; the PR author "
"must be a different user.",
"Pin the PR head SHA (gitea_view_pr) before validating.",
"Reconcile live queue state FIRST (do not trust prior handoffs): "
"call gitea_list_prs, then immediately gitea_view_pr on the target; "
"verify state, mergeable, head vs current, updated_at/merged info, "
"linked issue; git fetch --prune; git checkout master; git pull <remote> master --ff-only. "
"If prior report conflicts with live (e.g. 'merged' but live open, or head/updated mismatch), "
"report inconsistency and STOP before review/merge.",
"Pin the reviewed head SHA (gitea_view_pr) before validating.",
"Validate independently: scope vs the linked issue, tests, "
"diff check, secret sweep.",
"Post the verdict with gitea_review_pr / "
"gitea_submit_pr_review.",
"After any merge, re-list open PRs and re-view the PR to confirm landed state.",
"Do not merge unless the operator explicitly authorizes it.",
],
},