fix(gateway): working exits for wedged agents + declare_coverage roll-up unblock (#341)

A live task burned 5+ hours because every exit was locked. unclaim now
works from verifying and needs_revision (service guard + lifecycle edge);
the circuit breaker and the i_am_done push-failure remediate name the
working chain ending in unclaim(); sync_branch(stash=true) clears the
DIRTY_WORKSPACE dead-end (pop-conflict preserves the stash); blocking a
task QA already owns now says to idle instead of listing states; the
orchestrator auto-block logs real errors and skips states where blocking
is meaningless instead of force-blocking them.

declare_coverage (cell/main PM) retroactively stamps parent-AC refs on a
child that implements them -- closing the roll-up deadlock where the
declaring child was cancelled and its re-delegated replacement completed
the work uncredited. Cancelling a ref-declaring child now warns and
surfaces the orphaned criteria.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-08 21:40:22 +02:00
committed by GitHub
co-authored by Renn F
parent 889d48b99b
commit f48d088c08
34 changed files with 1514 additions and 69 deletions
+117
View File
@@ -1069,6 +1069,123 @@ async def test_rebase_onto_base_proceeds_on_clean_tree() -> None:
assert ["rebase", "origin/master"] in calls
# ---------------------------------------------------------------------------
# rebase_onto_base — stash=True auto-stash/pop (the dirty-workspace exit)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_rebase_onto_base_stash_true_auto_stashes_and_pops() -> None:
"""stash=True: a dirty tree is stashed (not refused), rebased, popped back."""
svc = _service()
calls: list[list[str]] = []
async def _run_git(_ws: object, args: list[str], **_kw: object) -> MagicMock:
calls.append(args)
res = MagicMock()
res.returncode = 0
if args[:2] == ["status", "--porcelain"]:
res.stdout = " M dirty.py\n"
elif args[:2] == ["rev-list", "--count"]:
res.stdout = "1"
else:
res.stdout = ""
return res
_bind(svc, "_run_git", AsyncMock(side_effect=_run_git))
result = await svc.rebase_onto_base(
Path("/tmp/ws"),
head_branch="feature/backend/h",
base_branch="master",
git_token="t",
stash=True,
)
assert result == {"status": "rebased", "unique_commits": 1}
push_args = ["stash", "push", "-u", "-m", "sync_branch autostash"]
pop_args = ["stash", "pop"]
assert push_args in calls
assert pop_args in calls
# Stash push runs before the rebase, pop runs after.
assert calls.index(push_args) < calls.index(["rebase", "origin/master"])
assert calls.index(pop_args) > calls.index(["rebase", "origin/master"])
@pytest.mark.asyncio
async def test_rebase_onto_base_stash_pop_conflict_preserves_stash() -> None:
"""A conflicted pop is flagged, never auto-resolved — stash stays intact."""
svc = _service()
async def _run_git(_ws: object, args: list[str], **_kw: object) -> MagicMock:
res = MagicMock()
res.returncode = 0
if args[:2] == ["status", "--porcelain"]:
res.stdout = " M dirty.py\n"
elif args[:2] == ["rev-list", "--count"]:
res.stdout = "1"
elif args == ["stash", "pop"]:
res.returncode = 1 # pop conflicted — stash is NOT dropped by git
else:
res.stdout = ""
return res
_bind(svc, "_run_git", AsyncMock(side_effect=_run_git))
result = await svc.rebase_onto_base(
Path("/tmp/ws"),
head_branch="feature/backend/h",
base_branch="master",
git_token="t",
stash=True,
)
assert result == {
"status": "rebased",
"unique_commits": 1,
"stash_pop_conflict": True,
}
@pytest.mark.asyncio
async def test_rebase_onto_base_stash_true_rebase_conflict_skips_pop() -> None:
"""A rebase conflict aborts before ever attempting the pop — no double
conflict; the stash is reported preserved for the caller to surface."""
svc = _service()
calls: list[list[str]] = []
async def _run_git(_ws: object, args: list[str], **_kw: object) -> MagicMock:
calls.append(args)
res = MagicMock()
res.returncode = 0
if args[:2] == ["status", "--porcelain"]:
res.stdout = " M dirty.py\n"
elif args == ["rebase", "origin/master"]:
res.returncode = 1
elif args[:2] == ["diff", "--name-only"]:
res.stdout = "src/a.py\n"
else:
res.stdout = ""
return res
_bind(svc, "_run_git", AsyncMock(side_effect=_run_git))
result = await svc.rebase_onto_base(
Path("/tmp/ws"),
head_branch="feature/backend/h",
base_branch="master",
git_token="t",
stash=True,
)
assert result == {
"status": "conflicts",
"files": ["src/a.py"],
"stash_preserved": True,
}
assert ["stash", "pop"] not in calls
# ---------------------------------------------------------------------------
# _link_commit_to_task — flush; the runner commits (no out-of-band commit)
# ---------------------------------------------------------------------------