Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .2119/verdicts/REQ-033.9.1--4ea88470081f.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "REQ-033.9.1--4ea88470081f",
"requirementId": "REQ-033.9.1",
"hash": "4ea88470081f",
"verdict": "pass",
"summary": "Independent review: the test pins the deferred-issue prefix against a separate literal covering title, PR and comment references, deferral reason, and implementation needs. The suffix permits only the workflow recovery-state substitution; weakening any required issue instruction fails.",
"timestamp": "2026-10-29T07:10:07.612Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.1.1--758c872f40db.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.1.1--758c872f40db",
"requirementId": "merge-recovery-state.1.1",
"hash": "758c872f40db",
"verdict": "pass",
"summary": "Independent review: The test extracts the rendered repair command in each 2119 workflow and executes a real free move; ITERATING or any non-BUILDING target fails, with fresh pending responsibilities verified.",
"timestamp": "2026-10-29T08:03:58.942Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.1.2--817908331560.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.1.2--817908331560",
"requirementId": "merge-recovery-state.1.2",
"hash": "817908331560",
"verdict": "pass",
"summary": "Independent review: Real GitHub workflow transitions and the independent full-policy literal reject a BUILDING target or other replacement for ITERATING.",
"timestamp": "2026-10-29T08:04:00.202Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.1.3--3c5c1b2643fb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.1.3--3c5c1b2643fb",
"requirementId": "merge-recovery-state.1.3",
"hash": "3c5c1b2643fb",
"verdict": "pass",
"summary": "Independent review: The complete ordered-policy comparison and single set_state directive guard reject unconditional BLOCKED recovery and added recovery branches during waits.",
"timestamp": "2026-10-29T08:04:01.454Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.2.1--5d0176bb1711.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.2.1--5d0176bb1711",
"requirementId": "merge-recovery-state.2.1",
"hash": "5d0176bb1711",
"verdict": "pass",
"summary": "Independent review: The full-policy comparison rejects removed evidence fields, stale reads, missing queue lookup, or missing head read-back; the CLI current-head query and mismatch restart establish the intended snapshot.",
"timestamp": "2026-10-29T08:04:02.767Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.2.2--39b864ba6c7d.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.2.2--39b864ba6c7d",
"requirementId": "merge-recovery-state.2.2",
"hash": "39b864ba6c7d",
"verdict": "pass",
"summary": "Independent review: The independently reviewed policy explicitly separates JSON buckets from process exits and forbids pending, optional failures, missing results, and query errors from proving failed required checks; mutations fail the full comparison.",
"timestamp": "2026-10-29T08:04:04.042Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.2.3--34950bab1104.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.2.3--34950bab1104",
"requirementId": "merge-recovery-state.2.3",
"hash": "34950bab1104",
"verdict": "pass",
"summary": "Independent review: Exact policy ordering and rejection of the old pre-queue section detect repair branches moved below waits, including known conflicts when check evidence is unavailable.",
"timestamp": "2026-10-29T08:04:05.338Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.2.4--ef8d81ec8034.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.2.4--ef8d81ec8034",
"requirementId": "merge-recovery-state.2.4",
"hash": "ef8d81ec8034",
"verdict": "pass",
"summary": "Independent review: The pinned empty-required-check clause rejects treating the recognized response as failed, unavailable, or pending and leaves other evidence to decide the action.",
"timestamp": "2026-10-29T08:04:06.532Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.3.1--699f500f11c5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.3.1--699f500f11c5",
"requirementId": "merge-recovery-state.3.1",
"hash": "699f500f11c5",
"verdict": "pass",
"summary": "Independent review: The full policy pins MERGING and artifact retention across all non-repair branches; inserting optimistic advancement or implementation recovery fails the comparison.",
"timestamp": "2026-10-29T08:04:07.771Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.3.2--55c5b2139115.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.3.2--55c5b2139115",
"requirementId": "merge-recovery-state.3.2",
"hash": "55c5b2139115",
"verdict": "pass",
"summary": "Independent review: The ordered unknown-BLOCKED branch must report the unresolved blocker and stop after identified waits are considered; retry loops or recovery substitutions fail.",
"timestamp": "2026-10-29T08:04:08.908Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.3.3--fd405999f6d6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.3.3--fd405999f6d6",
"requirementId": "merge-recovery-state.3.3",
"hash": "fd405999f6d6",
"verdict": "pass",
"summary": "Independent review: The pinned approval branch returns control without a watcher while the non-repair invariant preserves the artifact; watcher or deletion substitutions fail.",
"timestamp": "2026-10-29T08:04:10.091Z"
}
8 changes: 8 additions & 0 deletions .2119/verdicts/merge-recovery-state.3.4--abaa730bb10a.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"reviewId": "merge-recovery-state.3.4--abaa730bb10a",
"requirementId": "merge-recovery-state.3.4",
"hash": "abaa730bb10a",
"verdict": "pass",
"summary": "Independent review: The full policy rejects deleting or reordering the BEHIND synchronization branch or permitting it through the CLEAN/HAS_HOOKS enqueue gate.",
"timestamp": "2026-10-29T08:04:11.252Z"
}
64 changes: 64 additions & 0 deletions specs/merge-recovery-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Merge Recovery State

## Overview

The shared `babysit-merge` skill sends a pull request that needs repair back to implementation.
The GitHub peer-reviewed and self-reviewed workflows call that state `ITERATING`;
the three RFC 2119 workflows call it `BUILDING`. Recovery instructions need to name
the implementation state that exists in the active workflow. Waiting for approval,
pending checks, or the merge queue does not establish that implementation needs repair.

For this contract, repair evidence means a merge conflict (`mergeable=CONFLICTING`
or `mergeStateStatus=DIRTY`), a failed or cancelled latest required-check outcome
for the current PR head, or
`reviewDecision=CHANGES_REQUESTED`. A `BLOCKED` merge state alone is not repair
evidence. Failed or cancelled outcomes for older heads or superseded check runs
are not repair evidence. The contract concerns the rendered agent instructions; it does not add
a runtime PR classifier or a new workflow state.

## Requirements

### 1: Workflow-specific recovery

1. For `2119-human-spec`, `2119-auto-spec`, and `2119-auto-sol`, the rendered
`babysit-merge` instruction for an open PR with repair evidence MUST direct
`set_state BUILDING`.
2. For `github-peer-reviewed` and `github-self-reviewed`, the rendered
`babysit-merge` instruction for an open PR with repair evidence MUST direct
`set_state ITERATING`.
3. The rendered `babysit-merge` instructions MUST NOT direct recovery to an
implementation state without repair evidence.

Recovery uses the existing free-move contract in `REQ-009.5`: it bypasses the
normal transition graph and unresolved responsibilities, enters the destination,
and seeds fresh pending destination responsibilities.

### 2: Evidence before recovery

1. On each decision-tree invocation, the rendered `babysit-merge` instructions MUST
obtain fresh PR state, mergeability, merge state, review decision, queue membership,
and the latest required-check outcomes for the current PR head before deciding
whether an open PR needs repair.
2. The rendered `babysit-merge` instructions MUST NOT treat pending required checks,
failed optional checks, missing check results, or a check-query error as proof of
a failed required check.
3. For an open PR, the rendered `babysit-merge` instructions MUST evaluate repair
evidence before waiting for approval, checks, queue completion, or branch updates.
4. The rendered `babysit-merge` instructions MUST treat the recognized
`no required checks reported` response as an empty required-check set that by
itself establishes neither repair evidence, unavailable evidence, nor a
pending-check wait, leaving other PR evidence to determine the next action.

### 3: Waits preserve the merge stage

1. Without repair evidence, the rendered `babysit-merge` instructions MUST keep the
task in `MERGING` while approval is required, required checks are pending, the PR
is queued, the branch is behind, check evidence is unavailable, or a `BLOCKED`
merge state has no established cause.
2. For a `BLOCKED` open PR without repair evidence or an identified approval, check,
or queue wait, the rendered `babysit-merge` instructions MUST direct the agent to
report the unresolved blocker to the user and stop the current invocation.
3. For an approval wait without repair evidence, the rendered `babysit-merge`
instructions MUST return control to the user while retaining the state artifact.
4. For a behind branch without repair evidence, the rendered `babysit-merge`
instructions MUST require branch synchronization before attempting to queue the PR.
82 changes: 66 additions & 16 deletions src/panopticon/workflows/github_forge.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class GithubForgeWorkflow(PlannedWorkflow):
:meth:`plan_uri`, :meth:`_briefing_extras`) is inherited from
:class:`~panopticon.workflows.planned_workflow.PlannedWorkflow`."""

merge_recovery_state: ClassVar[str] = "ITERATING"

URL_RECORDED: ClassVar[Responsibility] = Responsibility(
key="url-recorded",
description="The PR URL is recorded on the task with the `set_url` MCP tool.",
Expand Down Expand Up @@ -158,32 +160,79 @@ def skills(self) -> Sequence[Skill]:
"none is running).\n"
"- `requeue_count`: how many times `gh pr merge --auto` has been called "
"(cap at 5).\n\n"
"## Pre-queue mergeability check\n\n"
"**Before touching the merge queue**, run:\n"
"`gh pr view <pr> --json state,mergeable,mergeStateStatus,autoMergeRequest`\n"
"If `mergeStateStatus` is `DIRTY` (conflicts) or `BEHIND`, stop and tell the user "
"to resolve that first with `babysit-ci`. Do not proceed.\n\n"
"## Decision tree (run on every entry and re-invocation)\n\n"
"**1. Fetch PR state** (always fresh):\n"
"`gh pr view <pr> --json state,mergeStateStatus,autoMergeRequest`\n\n"
"`gh pr view <pr> --json "
"state,headRefOid,mergeable,mergeStateStatus,reviewDecision,autoMergeRequest`\n"
"If this read fails or the PR state is unknown, retain MERGING and the state "
"artifact, report the unavailable evidence to the user, and stop.\n\n"
"**2. Branch on `state`:**\n\n"
"- **`MERGED`** → call the `advance` MCP operation (moves task to COMPLETE); "
"delete the state artifact; stop.\n"
"- **`CLOSED`** (not merged) → the PR was closed without merging. Do NOT advance. "
"Delete the state artifact, leave a clear message for the user, and stop "
"(the stop hook flips the turn to the user).\n"
"- **`OPEN`** → continue to step 3.\n\n"
"**3. Branch on merge state / queue membership:**\n\n"
"- `mergeStateStatus` is `BLOCKED` or `DIRTY`, or required checks are failing / "
"changes requested → call `set_state ITERATING` with an explanation; "
"**3. Fetch required-check and queue evidence** (always fresh):\n"
"`gh pr checks <pr> --required --json name,bucket,state,link`\n"
"`gh pr checks` queries the current PR head. After the check query, re-read "
"it with `gh pr view <pr> --json headRefOid`. If it differs from step 1, discard "
"the evidence and restart the decision tree at step 1. If the re-read fails, "
"mark check evidence unavailable.\n"
"Use only the latest outcome of each required check for the current "
"`headRefOid`. Ignore older-head and superseded failed/cancelled runs; if "
"current-head/latest scope cannot be established, mark check evidence "
"unavailable. Do not use optional-check failures as repair evidence.\n"
"Read the returned buckets: `fail` and `cancel` are repair evidence; `pending` "
"means wait; `pass` and `skipping` are not failures. Classify JSON buckets, "
"not exit status; a pending or nonzero exit alone is not repair evidence. The recognized "
"`no required checks reported` response means an empty required-check set: "
"it establishes neither repair evidence, unavailable evidence, nor a "
"pending-check wait; let the other PR evidence decide. Other query errors or "
"missing/invalid results mean evidence is unavailable, not failed checks.\n"
"Read actual queue membership separately (`autoMergeRequest` alone is not "
"queue membership):\n"
"`gh api graphql --field owner=<owner> --field name=<repo> --field number=<PR> --raw-field "
"query='query($owner:String!,$name:String!,$number:Int!){repository("
"owner:$owner,name:$name){pullRequest(number:$number){isInMergeQueue}}}'`\n"
"Use `data.repository.pullRequest.isInMergeQueue`; a query error or missing "
"value means queue evidence is unavailable. Do not infer false from a failed "
"read.\n\n"
"**4. Take the first matching branch** (repair before waits):\n\n"
"Without repair evidence, never move to an implementation state. Every "
"non-repair branch retains MERGING and the state artifact. A bare `BLOCKED` "
"or `UNKNOWN` value is not repair evidence.\n\n"
"- Repair evidence: `mergeable=CONFLICTING` or `mergeStateStatus=DIRTY`, "
"a latest current-head required-check bucket of `fail` or `cancel`, or "
"`reviewDecision=CHANGES_REQUESTED` → call "
f"`set_state {self.merge_recovery_state}` with an explanation; "
"delete the state artifact; stop.\n"
"- `autoMergeRequest` is non-null, or the PR is already in the merge queue → "
"**skip** `gh pr merge --auto` (do not double-queue); go directly to step 4.\n"
"- Otherwise → run `gh pr merge --squash --auto`; increment `requeue_count` in "
"the state artifact. If `requeue_count` > 5, bail to the user and stop.\n\n"
"**4. Arm background watcher:**\n"
"- If check or queue evidence is unavailable → report the unavailable evidence "
"to the user and stop. Independent known repair evidence still takes precedence "
"above.\n"
"- If `mergeStateStatus=BEHIND` → tell the user to synchronize the branch with "
"`babysit-ci` before attempting to queue; stop. Do not queue a behind branch.\n"
"- If `reviewDecision=REVIEW_REQUIRED` → tell the user approval is required and "
"stop, returning control to the user. Do not arm a check watcher for an "
"approval wait.\n"
"- If `autoMergeRequest` is non-null or `isInMergeQueue` is true → skip "
"`gh pr merge --auto` (do not double-queue). If required checks are pending, "
"go to step 5; otherwise tell the user to re-invoke `babysit-merge` after queue "
"completion to observe `MERGED` and advance the task, then stop. A "
"completed-check watcher does not wait for a merge event.\n"
"- If required checks are pending → go to step 5 without queueing yet.\n"
"- If `mergeStateStatus=BLOCKED`, mergeability is unknown, or readiness is "
"otherwise unestablished → report the unresolved blocker to the user and stop. "
"Do not enqueue or loop on unknown evidence.\n"
"- Otherwise, only when mergeability is established and `mergeStateStatus` "
"is `CLEAN` or `HAS_HOOKS` → run `gh pr merge --squash --auto`; increment "
"`requeue_count` in the state artifact. If `requeue_count` > 5, bail to the "
"user and stop. After enqueueing, tell the user to re-invoke `babysit-merge` "
"after queue completion to observe `MERGED` and advance the task, then stop "
"with the state artifact retained.\n\n"
"**5. Arm background watcher** (only for known pending required checks):\n"
"```\n"
"run_in_background: gh pr checks <PR> --watch 2>&1 "
"run_in_background: gh pr checks <PR> --required --watch 2>&1 "
"| tee /tmp/babysit-merge-watch.log; "
"echo EXIT:$? >> /tmp/babysit-merge-watch.log\n"
"```\n"
Expand All @@ -194,7 +243,8 @@ def skills(self) -> Sequence[Skill]:
"user, delete the state artifact, and stop.\n"
"2. If `watch_bash_id` is set and that background Bash task is **still running** → "
"end the turn immediately (nothing to do yet).\n"
"3. Read `/tmp/babysit-merge-watch.log`. Find the last `EXIT:N` line.\n"
"3. If a watcher completed, read `/tmp/babysit-merge-watch.log` and find the "
"last `EXIT:N` line. A prior wait may have no watcher or log.\n"
"4. Clear `watch_bash_id` in the state artifact (`null`).\n"
"5. Re-enter the decision tree from step 1 above. Repeat until merged, blocked, "
"or budget exceeded.",
Expand Down
2 changes: 2 additions & 0 deletions src/panopticon/workflows/spec_2119.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ class _Merging(State):
class _Spec2119Workflow(GithubForgeWorkflow):
"""Shared forge skills for the 2119 workflow family."""

merge_recovery_state: ClassVar[str] = "BUILDING"

#: Whether stage-4 adversarial review uses Fable plus Sol; test honesty always uses Sol.
fable_reviews: ClassVar[bool] = True
reviewers: ClassVar[tuple[ReviewerConfig, ReviewerConfig]] = (
Expand Down
Loading
Loading