Skip to content
Merged
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
109 changes: 79 additions & 30 deletions contract_tests/test_claiming.py
Original file line number Diff line number Diff line change
Expand Up @@ -2418,7 +2418,7 @@ def test_blocked_result_replaces_older_candidate_with_latest_verified_push(self)
self.assertEqual(work["claim"]["candidate"]["head_revision"], latest_head)
reconstruct_mailbox(checkout)

def test_inherited_candidate_survives_pr_block_release_reclaim_and_delivery(self) -> None:
def test_denied_pr_ack_recovery_survives_release_reclaim_and_delivery(self) -> None:
def fresh_coordinator() -> ClaimCoordinator:
return ClaimCoordinator(
str(self.mailbox_remote),
Expand Down Expand Up @@ -2601,9 +2601,6 @@ def fresh_coordinator() -> ClaimCoordinator:
)

self.coordinator = fresh_coordinator()
advanced_claim = self.claim(token="head-advance-token-0")
advanced_delegation = self.delegation()
advanced_invocation = self.delegated_invocation(advanced_claim)
git(
self.project_checkout,
"-c",
Expand All @@ -2613,11 +2610,31 @@ def fresh_coordinator() -> ClaimCoordinator:
"commit",
"--allow-empty",
"-m",
"Advance inherited candidate",
"Advance canonical base",
)
advanced_head = git(self.project_checkout, "rev-parse", "HEAD").stdout.strip()
git(self.project_checkout, "push", "origin", f"{advanced_head}:{candidate_ref}")
advanced_candidate = {**candidate, "head_sha": advanced_head}
git(self.project_checkout, "push", "origin", "HEAD:main")
advanced_claim = self.claim(token="head-advance-token-0")
advanced_delegation = self.delegation()
advanced_invocation = self.delegated_invocation(advanced_claim)
advanced_base = advanced_invocation["repository"]["base_sha"]
self.assertNotEqual(advanced_base, candidate["base_sha"])
candidate_tree = git(self.project_checkout, "rev-parse", "HEAD^{tree}").stdout.strip()
advanced_head = git(
self.project_checkout,
"-c",
"user.name=Atelier Test",
"-c",
"user.email=atelier-test@invalid",
"commit-tree",
candidate_tree,
"-m",
"Rewrite inherited candidate",
).stdout.strip()
advanced_candidate = {
**candidate,
"base_sha": advanced_base,
"head_sha": advanced_head,
}
advanced_terminal_candidate = {
**advanced_candidate,
"publication": {
Expand All @@ -2643,29 +2660,52 @@ def fresh_coordinator() -> ClaimCoordinator:
token="head-advance-token-1",
candidate=advanced_candidate,
)
advanced_published = self.delegated_checkpoint(
advanced_delegation,
git(
self.project_checkout,
"push",
"--force",
"origin",
f"{advanced_head}:{candidate_ref}",
)
recoverable_candidate = {
**advanced_candidate,
"publication": {"kind": "ordinary", "pull_requests": []},
}
denied_publication = self.delegated_request(
advanced_invocation,
advanced_push,
action="repository.candidate.push",
token="head-advance-token-2",
phase="candidate_published",
candidate=advanced_terminal_candidate,
candidate=recoverable_candidate,
)
denial = self.assert_checkpoint_denied_without_mutation(
advanced_delegation,
advanced_invocation,
denied_publication,
)
self.assertIn(
"candidate pull request metadata lacks exact pre-mutation authority",
denial["reason"],
)
advanced_blocked_result = self.blocked_result(
advanced_invocation,
advanced_published,
candidate=advanced_terminal_candidate,
advanced_push,
candidate=recoverable_candidate,
authority_used=["repository.candidate.push"],
)
advanced_blocked_result["blocking_reason"] = (
"The same pull request remains blocked after its candidate head advanced."
"The candidate push succeeded, but publication acknowledgement was denied "
"because the existing pull request lacked fresh exact authority."
)
advanced_blocked_result["unresolved_obligations"] = [
"Release and reclaim the exact pushed candidate.",
"Obtain fresh pull_request.update authority before restoring the existing PR.",
]
advanced_delegation.finalize(
self.work_id,
advanced_invocation,
advanced_blocked_result,
self.fence(advanced_published),
self.fence(advanced_push),
approved_commit=self.approved_commit,
policy_target=self.policy_target(),
host_target=self.delegation_host_target(),
Expand All @@ -2689,41 +2729,50 @@ def fresh_coordinator() -> ClaimCoordinator:
"remote": "origin",
"remote_url": candidate["remote_url"],
"remote_ref": candidate_ref,
"base_revision": candidate["base_sha"],
"base_revision": advanced_base,
"head_revision": advanced_head,
"pull_request": pull_request_url,
"pull_request": None,
"workspace_id": None,
"published_at": (self.live_at + timedelta(minutes=2))
"published_at": (self.live_at + timedelta(minutes=3))
.isoformat()
.replace("+00:00", "Z"),
},
)
self.assertEqual(advanced_work["claim"]["inherited_receipt_id"], release_id)

advanced_work["claim"]["inherited_receipt_id"] = first_release_id
fixtures.write_markdown(
advanced_checkout / f"work/{self.work_id}/work.md",
advanced_work,
blocked_receipt, _ = _read_yaml(
advanced_checkout
/ f"work/{self.work_id}/receipts/{advanced_work['attempt_receipt_id']}.md",
frontmatter=True,
label="denied acknowledgement receipt",
)
prior_receipt, _ = _read_yaml(
advanced_checkout / f"work/{self.work_id}/receipts/{release_id}.md",
frontmatter=True,
label="prior PR-bearing receipt",
)
self.assertIsNone(blocked_receipt["candidate"]["pull_request"])
self.assertEqual(
blocked_receipt["unresolved_obligations"],
advanced_blocked_result["unresolved_obligations"],
)
with self.assertRaisesRegex(MailboxValidationError, "checkpoint-pr-authority"):
reconstruct_mailbox(advanced_checkout)
self.assertEqual(prior_receipt["candidate"]["pull_request"], pull_request_url)

advanced_release_id = new_identifier("rcp")
self.coordinator.release(
self.work_id,
self.fence(advanced_published),
self.fence(advanced_push),
receipt_id=advanced_release_id,
reason="Transfer the same-PR advanced candidate to a final worker.",
reason="Transfer the denied-acknowledgement candidate to a final worker.",
ended_at=OBSERVED_AT + timedelta(minutes=8),
)
self.coordinator = fresh_coordinator()
final_claim = self.claim(token="ready-pr-token-0")
final_delegation = self.delegation()
final_invocation = self.delegated_invocation(final_claim)
replacement_pull_request_url = "https://github.com/example/project-1/pull/795"
replacement_pull_request_url = pull_request_url
replacement_terminal_candidate = json.loads(json.dumps(advanced_terminal_candidate))
replacement_pull_request = replacement_terminal_candidate["publication"]["pull_requests"][0]
replacement_pull_request["id"] = "795"
replacement_pull_request["id"] = "794"
replacement_pull_request["url"] = replacement_pull_request_url
replacement_authorized = self.delegated_checkpoint(
final_delegation,
Expand Down
12 changes: 8 additions & 4 deletions skills/atelier/references/delegation.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,14 @@ match the still-open current native ticket before recording a receipt.
- `blocked` preserves any acknowledged candidate. If the exact push succeeded but
its `candidate_published` acknowledgement failed, the blocked result may recover
that remotely reachable candidate only when it matches the sealed invocation and
the ledger's final authorized push head and remote ref. Atelier binds the candidate
and immutable blocked receipt atomically, records one unresolved planner decision,
and retains
mutation ownership.
the ledger's final authorized push head and remote ref. A replacement candidate
recovered this way may omit an inherited pull request that could not be
acknowledged without fresh exact PR authority; the predecessor receipt preserves
that prior candidate and PR history, while the blocked receipt preserves the
denied-acknowledgement obligations. Atelier binds the candidate and immutable
blocked receipt atomically, records one unresolved planner decision, and retains
mutation ownership. A later attempt still needs fresh exact PR authority before
it can publish or restore PR metadata.
- `requires_epic` is recorded as a blocked attempt for planner action. Atelier
does not invoke `implement-epic` from delegated work.

Expand Down
62 changes: 39 additions & 23 deletions skills/atelier/scripts/delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,41 +798,57 @@ def _blocked_candidate(
return None
pull_request = _publication_pull_request(candidate)
current_pull_request = current["pull_request"] if current is not None else None
if pull_request != current_pull_request and not _pull_request_mutation_authorized(
claim,
candidate_head=candidate["head_sha"],
candidate_remote_ref=candidate["remote_ref"],
):
raise MailboxTransitionRejected(
"blocked pull request metadata lacks fresh exact pre-mutation authority"
)
candidate_identity = (
candidate["repository"],
candidate["remote_url"],
candidate["remote_ref"],
candidate["base_sha"],
candidate["head_sha"],
)
if current is not None and candidate_identity == (
current["repository"],
current["remote_url"],
current["remote_ref"],
current["base_revision"],
current["head_revision"],
current_identity = (
(
current["repository"],
current["remote_url"],
current["remote_ref"],
current["base_revision"],
current["head_revision"],
)
if current is not None
else None
)
ledger = claim["checkpoint"]["authorizations"]
recovers_exact_push = (
result["implementation_state"] == "published"
and bool(ledger)
and ledger[-1]["phase"] == "pre_external_mutation"
and ledger[-1]["action"] == "repository.candidate.push"
and ledger[-1]["candidate_head"] == candidate["head_sha"]
and ledger[-1]["candidate_remote_ref"] == candidate["remote_ref"]
)
drops_prior_pull_request_for_recovery = (
current_pull_request is not None
and pull_request is None
and candidate_identity != current_identity
and recovers_exact_push
)
if (
pull_request != current_pull_request
and not drops_prior_pull_request_for_recovery
and not _pull_request_mutation_authorized(
claim,
candidate_head=candidate["head_sha"],
candidate_remote_ref=candidate["remote_ref"],
)
):
raise MailboxTransitionRejected(
"blocked pull request metadata lacks fresh exact pre-mutation authority"
)
if current is not None and candidate_identity == current_identity:
value = copy.deepcopy(current)
if pull_request is not None:
value["pull_request"] = pull_request
return value
ledger = claim["checkpoint"]["authorizations"]
if (
result["implementation_state"] != "published"
or not ledger
or ledger[-1]["phase"] != "pre_external_mutation"
or ledger[-1]["action"] != "repository.candidate.push"
or ledger[-1]["candidate_head"] != candidate["head_sha"]
or ledger[-1]["candidate_remote_ref"] != candidate["remote_ref"]
):
if not recovers_exact_push:
raise MailboxTransitionRejected(
"blocked published candidate lacks its exact push authorization"
)
Expand Down
60 changes: 46 additions & 14 deletions skills/atelier/scripts/mailbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,36 @@ def same_lineage(left: dict[str, Any] | None, right: dict[str, Any] | None) -> b
right["base_revision"],
)

def same_remote_lineage(
left: dict[str, Any] | None,
right: dict[str, Any] | None,
) -> bool:
return left is not None and right is not None and (
left["repository"],
left["remote"],
left["remote_url"],
left["remote_ref"],
) == (
right["repository"],
right["remote"],
right["remote_url"],
right["remote_ref"],
)

recovered_current_candidate = (
attempt_receipt is not None
and attempt_receipt["outcome"] == "blocked"
and attempt_receipt["handoff"] == "transferable"
and attempt_receipt["claim_id"] == claim["id"]
and attempt_receipt["candidate"] == candidate
and bool(ledger)
and ledger[-1]["phase"] == "pre_external_mutation"
and ledger[-1]["action"] == "repository.candidate.push"
and candidate is not None
and ledger[-1]["candidate_head"] == candidate["head_revision"]
and ledger[-1]["candidate_remote_ref"] == candidate["remote_ref"]
)

inherited_receipt_id = claim["inherited_receipt_id"]
inherited_receipt = (
work_receipts.get(inherited_receipt_id) if inherited_receipt_id is not None else None
Expand All @@ -978,7 +1008,13 @@ def same_lineage(left: dict[str, Any] | None, right: dict[str, Any] | None) -> b
inherited_receipt["claim_id"] == claim["id"]
or inherited_receipt["handoff"] != "transferable"
or inherited_receipt["candidate"] is None
or not same_lineage(inherited_receipt["candidate"], candidate)
or (
not same_lineage(inherited_receipt["candidate"], candidate)
and not (
recovered_current_candidate
and same_remote_lineage(inherited_receipt["candidate"], candidate)
)
)
):
diagnostics.append(
Diagnostic(
Expand All @@ -1002,19 +1038,6 @@ def same_lineage(left: dict[str, Any] | None, right: dict[str, Any] | None) -> b
"claim does not bind the current transferable predecessor receipt",
)
)
recovered_current_candidate = (
attempt_receipt is not None
and attempt_receipt["outcome"] == "blocked"
and attempt_receipt["handoff"] == "transferable"
and attempt_receipt["claim_id"] == claim["id"]
and attempt_receipt["candidate"] == candidate
and bool(ledger)
and ledger[-1]["phase"] == "pre_external_mutation"
and ledger[-1]["action"] == "repository.candidate.push"
and candidate is not None
and ledger[-1]["candidate_head"] == candidate["head_revision"]
and ledger[-1]["candidate_remote_ref"] == candidate["remote_ref"]
)
acknowledged_source = (
inherited_candidate_source
if same_identity(inherited_candidate_source, candidate)
Expand Down Expand Up @@ -1174,6 +1197,14 @@ def same_lineage(left: dict[str, Any] | None, right: dict[str, Any] | None) -> b
for entry in ledger
)
)
blocked_push_drops_inherited_pull_request = (
recovered_current_candidate
and inherited_candidate_source is not None
and candidate is not None
and not same_identity(inherited_candidate_source, candidate)
and inherited_candidate_source["pull_request"] is not None
and candidate["pull_request"] is None
)
current_unacknowledged = (
candidate is not None
and not inherited_candidate
Expand Down Expand Up @@ -1220,6 +1251,7 @@ def same_lineage(left: dict[str, Any] | None, right: dict[str, Any] | None) -> b
candidate is not None
and candidate["pull_request"] != latest_published_pull_request
and not terminal_pull_request_normalization
and not blocked_push_drops_inherited_pull_request
):
diagnostics.append(
Diagnostic(
Expand Down
Loading