fix(git): pass actor_agent_id to pr_merge for workspace resolution

pr_merge falls back to task.assigned_to for workspace resolution, but
that field is None at merge time (submit_qa/pass_qa cleared it during
prior transitions). When project.workspace_path is unset, the resolver
raises ValidationError 'no workspace configured and no agent_id
provided' — surfaces as 500 from cell_pm_complete.

Add actor_agent_id parameter (the PM doing the merge) and use it as
the primary workspace owner. Falls back to task.assigned_to, then
created_by, before raising. cell_pm_complete now threads pm_agent_id
through.
This commit is contained in:
Renn F
2026-05-03 22:49:12 +02:00
parent 24279fbee4
commit ee743ffc7a
3 changed files with 24 additions and 5 deletions
+3 -1
View File
@@ -2145,7 +2145,9 @@ class Choreographer:
verb="cell_pm_complete", verb="cell_pm_complete",
) )
target = parent_branch_for(t.branch_name) target = parent_branch_for(t.branch_name)
merge_result = await self.git.pr_merge(t.pr_number, target=target) merge_result = await self.git.pr_merge(
t.pr_number, target=target, actor_agent_id=pm_agent_id
)
leaf_parent_id = t.parent_task_id leaf_parent_id = t.parent_task_id
leaf_team = t.team leaf_team = t.team
t = await self.task.cell_pm_complete( t = await self.task.cell_pm_complete(
+18 -3
View File
@@ -1740,7 +1740,13 @@ class GitService(BaseService):
.with_for_update(of=_TaskTable) .with_for_update(of=_TaskTable)
) )
async def pr_merge(self, pr_number: int, *, target: str) -> dict[str, Any]: async def pr_merge(
self,
pr_number: int,
*,
target: str,
actor_agent_id: UUID | None = None,
) -> dict[str, Any]:
"""Merge PR `pr_number` into `target`. """Merge PR `pr_number` into `target`.
Returns: ``{"merge_commit_sha": str | None}``. Looks up the Returns: ``{"merge_commit_sha": str | None}``. Looks up the
@@ -1768,9 +1774,18 @@ class GitService(BaseService):
if project is None: if project is None:
raise NotFoundError("Project", str(task.project_id)) raise NotFoundError("Project", str(task.project_id))
# Workspace resolution priority: caller-provided actor (the PM
# doing the merge) > task.assigned_to > created_by. assigned_to
# is often None at merge time because submit_qa / pass_qa cleared
# it during prior transitions; without a fallback the resolver
# raises ValidationError when project.workspace_path is unset.
workspace_agent_id = actor_agent_id or (
UUID(str(task.assigned_to)) if task.assigned_to else None
)
if workspace_agent_id is None and task.created_by:
workspace_agent_id = UUID(str(task.created_by))
workspace = await self.get_workspace( workspace = await self.get_workspace(
project.slug, project.slug, agent_id=workspace_agent_id
agent_id=UUID(str(task.assigned_to)) if task.assigned_to else None,
) )
git_token = await self._get_project_token_or_raise(project.slug) git_token = await self._get_project_token_or_raise(project.slug)
owner, repo = self._parse_github_remote(workspace) owner, repo = self._parse_github_remote(workspace)
+3 -1
View File
@@ -263,7 +263,9 @@ async def test_cell_pm_complete_merges_then_completes() -> None:
env = await c.cell_pm_complete(pm_id, task_id, notes="reviewed and approved") env = await c.cell_pm_complete(pm_id, task_id, notes="reviewed and approved")
assert env.error is None assert env.error is None
assert env.status == "completed" assert env.status == "completed"
git_svc.pr_merge.assert_awaited_once_with(8, target="feature/backend/abc") git_svc.pr_merge.assert_awaited_once_with(
8, target="feature/backend/abc", actor_agent_id=pm_id
)
@pytest.mark.asyncio @pytest.mark.asyncio