Skip to content

feat(pr-management): add triage history and guarded recovery - #2065

Open
ameeer-in wants to merge 1 commit into
apache:mainfrom
ameeer-in:feat/triage-session-recovery
Open

ameeer-in wants to merge 1 commit into
apache:mainfrom
ameeer-in:feat/triage-session-recovery

Conversation

@ameeer-in

Copy link
Copy Markdown

Summary

  • Add structured session-local action history so maintainers can revisit applied or skipped PR decisions.
  • Record operation intent and verified outcomes, then preview confirmed recovery of reversible changes while preserving unrelated edits.
  • Handle partial failures, uncertain outcomes, legacy caches, and irreversible effects explicitly.

Type of change

  • Skill change with behavioral eval coverage
  • Python package
  • Tool / bridge contract
  • Documentation and specification updates

Test plan

  • Whole-tree hooks passed, excluding separately scoped package tests
  • Branch-scoped package tests passed: 1,427 tests
  • Ruff, formatting, mypy, link checks, Apache RAT 0.18, and zizmor passed
  • Behavioral evals passed: 54/54 on Claude and 21/21 on Codex
  • Regression tests and local CLI integration against a fake forge cover compound recovery, conflicting changes, partial failures, and repeated recovery

Independent correctness, security, and conventions reviews completed with no remaining findings.

RFC-AI-0004 compliance

  • HITL — recovery requires an explicit preview and confirmation
  • Sandbox — added reads remain scoped to the configured upstream
  • Vendor neutrality — project-specific values remain configuration-driven
  • Conversational + correctable — history supports inspection and reclassification
  • Write-access discipline — no autonomous recovery writes

Linked issues

Closes #74

Gen-AI disclosure

Codex assisted with implementation, testing.
I reviewed and responsible for the contribution.

Record operation intent and verified results in a session-local action log
so maintainers can revisit earlier PR decisions and recover reversible
changes without discarding unrelated edits.

Keep uncertain writes and irreversible effects explicit, preserve legacy
session caches, and reuse the existing readiness guards and atomic writes.

Closes apache#74

Generated-by: Codex (GPT-6)

@Kaap10 Kaap10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for working on this! Adding structured action journaling and guarded recovery directly addresses #74 and is a great ergonomics and safety improvement.

The overall design - evidence-based receipts, reusing readiness guards, digest verification, and dual-tree parity is very solid.

I tested the branch locally and left a few inline comments on edge cases (mainly a Windows test assertion failure and minor recovery/formatting details).

result = recovery.history(path, REPO, VIEWER)
assert [a["pr"] for a in result["actions"]] == [12, 13]
assert "Please fix CI" not in json.dumps(result)
assert path.stat().st_mode & 0o077 == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This assertion fails when running tests on Windows / NTFS:

assert path.stat().st_mode & 0o077 == 0
# AssertionError: assert (33206 & 63) == 0

On Windows, os.stat().st_mode only reflects read-only flags and does not support POSIX group/other permission bits (0o077).

Could we guard this check for POSIX platforms?

if sys.platform != "win32":
    assert path.stat().st_mode & 0o077 == 0

(Also mirror the same guard to any other file permission assertions in this suite).

if current != expected:
raise ValueError("the Magpie block changed after this action")
if current is not None:
return body.replace(current, previous or "", 1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When recovering from an action that added a brand new managed block (i.e. previous=None or ""), replacing current directly with "" leaves behind the two leading newlines (\n\n) that splice() originally prepended.

While this doesn't break parsing, it can cause trailing blank lines to accumulate at the bottom of the PR description if an issue undergoes multiple triage and recovery cycles. Consider stripping trailing whitespace or normalizing trailing newlines when previous is falsy.

for a in log["actions"]
]
recorded = {str(a["pr"]) for a in log["actions"]}
legacy = {**data["prs"], **log.get("legacy_prs", {})}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if pr not in recorded or pr in log.get("legacy_prs", {}):

If a PR was previously recorded in data["prs"] as a legacy entry and then acted upon in the current session (adding it to both log["actions"] and log["legacy_prs"]), checking pr in log.get("legacy_prs", {}) will append a duplicate row with status: "legacy" alongside the action entry.

Is this intentional so maintainers can see the pre-journal historical state, or should active session actions take precedence in the history listing?

raise ValueError("--revisit of a journaled PR needs upstream_repo in project.md")
log = triage_recovery.history(Path(args.session), cfg.upstream_repo, args.viewer)
logged_prs = {entry["pr"] for entry in log["actions"]}
if str(args.revisit) not in session and args.revisit not in logged_prs:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if str(args.revisit) not in session and args.revisit not in logged_prs:

logged_prs is populated with entry["pr"] which are ints, whereas keys in session are strings (e.g. "12").

The membership checks str(args.revisit) not in session and args.revisit not in logged_prs handle the types correctly here, but for consistency and defensive parsing across legacy caches, casting both to str (or both to int) prevents any subtle KeyError edge cases when loading handwritten or migrated session caches.

or comment.get("updated_at") != effect["comment_updated_at"]
):
raise ValueError(f"comment {ident} changed or belongs to another actor/PR")
return {"argv": ["gh", "api", f"repos/{repo}/issues/comments/{ident}", "-X", "DELETE"]}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verifying all four invariants (issue_url, user.login, body, and updated_at) before returning the DELETE command is an excellent safety guard. It ensures we never accidentally delete a comment that was subsequently edited by the author or that belongs to a different thread. Nicely done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Triage CLI: 'move back' affordance to revisit / undo previously applied actions

2 participants