Skip to content

Commit 088ccfb

Browse files
authored
fix(orchestration): bind review and acceptance repositories (#13)
* fix(orchestration): diff bound execution repository * fix(orchestration): bind plan acceptance to repository * fix(orchestration): select terminal acceptance repository * fix(orchestration): require terminal acceptance evidence * fix(orchestration): fail closed on terminal provenance
1 parent 03fc5be commit 088ccfb

4 files changed

Lines changed: 350 additions & 7 deletions

File tree

scripts/orchestration/execution_context.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,10 +1548,14 @@ def build_review_package(args: argparse.Namespace) -> Path:
15481548
handoff, _ = _read_structured(handoff_path)
15491549
validated = validate_executor_result_for_task(handoff, task, observe=True, **_observation_kwargs(args))
15501550
knowledge_disposition = validated["knowledge_disposition"]
1551+
binding = load_task_execution_binding(root, plan_id, task_id)
1552+
execution_root = Path(str(binding["execution_path"])).resolve()
15511553

1552-
base = _resolve_commit(root, str(args.base))
1554+
base = _resolve_commit(execution_root, str(args.base))
15531555
write_paths = [str(path) for path in _as_list((task.get("files") or {}).get("write"))]
1554-
head, diff, name_status, out_of_scope = _review_diff(root, base, str(args.head), write_paths)
1556+
head, diff, name_status, out_of_scope = _review_diff(
1557+
execution_root, base, str(args.head), write_paths
1558+
)
15551559
if len(diff.encode("utf-8")) > MAX_DIFF_BYTES or diff.count("\n") > MAX_DIFF_LINES:
15561560
oversized = ", ".join(
15571561
sorted({path for line in name_status for path in _paths_from_name_status(line)})

scripts/orchestration/plans.py

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,115 @@ def _handoff_recorded_identities(handoff: dict[str, object]) -> list[str]:
185185

186186

187187
def _verified_handoff_tree(root: Path, handoff: dict[str, object]) -> str | None:
188+
repositories = handoff.get("repository") if isinstance(handoff.get("repository"), list) else []
189+
for repository in repositories:
190+
if not isinstance(repository, dict):
191+
continue
192+
recorded_root = str(repository.get("root") or "").strip()
193+
metadata = repository.get("metadata") if isinstance(repository.get("metadata"), dict) else {}
194+
identity = str(metadata.get("actual_commit") or "").strip()
195+
if recorded_root and identity:
196+
tree = _git_tree_id(Path(recorded_root).expanduser().resolve(), identity)
197+
if tree:
198+
return tree
188199
for identity in _handoff_recorded_identities(handoff):
189200
tree = _git_tree_id(root, identity)
190201
if tree:
191202
return tree
192203
return None
193204

194205

206+
def _plan_task_order(args: argparse.Namespace, plan_id: str) -> dict[str, int]:
207+
order: dict[str, int] = {}
208+
for row in index_plans(args):
209+
if row.get("type") != "task" or row.get("plan_id") != plan_id:
210+
continue
211+
front_matter, _body = read_front_matter(artifact_path_from_row(row, args))
212+
task_id = str(front_matter.get("id") or row.get("id") or "")
213+
value = front_matter.get("order")
214+
rank: int | None = None
215+
if isinstance(value, int) and not isinstance(value, bool):
216+
rank = value
217+
elif isinstance(value, str) and re.fullmatch(r"[1-9]\d*", value.strip()):
218+
rank = int(value)
219+
if task_id and rank is not None:
220+
if rank in order.values():
221+
raise SystemExit("acceptance-blocked: final plan task order is ambiguous")
222+
order[task_id] = rank
223+
return order
224+
225+
226+
def _material_repository_root(
227+
args: argparse.Namespace,
228+
plan_id: str,
229+
validated: list[tuple[dict[str, object], dict[str, object]]],
230+
commands: list[str],
231+
) -> Path:
232+
entries: list[tuple[Path, str]] = []
233+
material = [pair for pair in validated if _handoff_has_material_changes(*pair)]
234+
if not material:
235+
return project_root(args)
236+
for handoff, _brief in material:
237+
handoff_has_provenance = False
238+
repositories = handoff.get("repository") if isinstance(handoff.get("repository"), list) else []
239+
for repository in repositories:
240+
if not isinstance(repository, dict):
241+
continue
242+
recorded = str(repository.get("root") or "").strip()
243+
metadata = repository.get("metadata") if isinstance(repository.get("metadata"), dict) else {}
244+
identity = str(metadata.get("actual_commit") or "").strip()
245+
if recorded and identity:
246+
entries.append((Path(recorded).expanduser().resolve(), identity))
247+
handoff_has_provenance = True
248+
if not handoff_has_provenance:
249+
try:
250+
fallback = _resolve_final_plan_workspace(args)
251+
except SystemExit as error:
252+
raise SystemExit(
253+
"acceptance-blocked: material handoff repository provenance is unavailable"
254+
) from error
255+
entries.append((fallback, "HEAD"))
256+
roots = {root for root, _identity in entries}
257+
if len(roots) == 1:
258+
return next(iter(roots))
259+
task_order = _plan_task_order(args, plan_id)
260+
material_ranks: list[int] = []
261+
for handoff, brief in validated:
262+
if not _handoff_has_material_changes(handoff, brief):
263+
continue
264+
task_id = str(brief.get("task_id") or "")
265+
if task_id not in task_order:
266+
raise SystemExit("acceptance-blocked: final plan task order is unavailable")
267+
material_ranks.append(task_order[task_id])
268+
terminal_material_rank = max(material_ranks) if material_ranks else -1
269+
acceptance_entries: list[tuple[Path, str]] = []
270+
for handoff, brief in validated:
271+
if not any(_handoff_command_result(handoff, command) == "passed" for command in commands):
272+
continue
273+
task_id = str(brief.get("task_id") or "")
274+
rank = task_order.get(task_id)
275+
if rank is None or rank < terminal_material_rank:
276+
continue
277+
repositories = handoff.get("repository") if isinstance(handoff.get("repository"), list) else []
278+
for repository in repositories:
279+
if not isinstance(repository, dict):
280+
continue
281+
recorded = str(repository.get("root") or "").strip()
282+
metadata = repository.get("metadata") if isinstance(repository.get("metadata"), dict) else {}
283+
identity = str(metadata.get("actual_commit") or "").strip()
284+
if recorded and identity:
285+
acceptance_entries.append((Path(recorded).expanduser().resolve(), identity))
286+
fresh_acceptance_roots: set[Path] = set()
287+
for root, identity in acceptance_entries:
288+
head_tree = _git_tree_id(root, "HEAD")
289+
recorded_tree = _git_tree_id(root, identity)
290+
if head_tree is not None and recorded_tree is not None and head_tree == recorded_tree:
291+
fresh_acceptance_roots.add(root)
292+
if len(fresh_acceptance_roots) == 1:
293+
return next(iter(fresh_acceptance_roots))
294+
raise SystemExit("acceptance-blocked: final plan repository is ambiguous")
295+
296+
195297
def _acceptance_result_detail(results: set[str]) -> str:
196298
if not results:
197299
return "missing"
@@ -280,7 +382,7 @@ def _assert_archive_plan_acceptance(
280382
commands = _declared_integration_commands(body)
281383
if not commands:
282384
return
283-
git_root = project_root(args)
385+
git_root = _material_repository_root(args, plan_id, validated, commands)
284386
terminal_tree = _git_tree_id(git_root, "HEAD")
285387
material = [pair for pair in validated if _handoff_has_material_changes(*pair)]
286388
for command in commands:
@@ -309,7 +411,7 @@ def _assert_archive_plan_acceptance(
309411
raise SystemExit(
310412
f"acceptance-blocked: declared plan-level acceptance {command} is {_acceptance_result_detail(judged)}"
311413
)
312-
workspace = _resolve_final_plan_workspace(args)
414+
workspace = git_root if material else _resolve_final_plan_workspace(args)
313415
for command in commands:
314416
_assert_archive_command_state_neutral(command, workspace)
315417

@@ -457,9 +559,22 @@ def cmd_archive_plan(args: argparse.Namespace) -> None:
457559
replace_front_matter_value(root_path, "status", "Completed")
458560
moved.append(move_to_archive(root_path, active_root, archived_root))
459561

460-
active_plan_dir = active_root / args.id
562+
sibling_plan_dir = root_path.with_suffix("")
563+
indexed_active_dirs = {
564+
active_root / artifact_path_from_row(row, args).relative_to(active_root).parts[0]
565+
for row in rows
566+
if row.get("type") == "task"
567+
and row.get("plan_id") == args.id
568+
and is_relative_to(artifact_path_from_row(row, args), active_root)
569+
}
570+
if sibling_plan_dir.is_dir() and is_relative_to(sibling_plan_dir, active_root):
571+
active_plan_dir = sibling_plan_dir
572+
elif len(indexed_active_dirs) == 1:
573+
active_plan_dir = next(iter(indexed_active_dirs))
574+
else:
575+
active_plan_dir = active_root / args.id
461576
if active_plan_dir.exists():
462-
archived_plan_dir = archived_root / args.id
577+
archived_plan_dir = archived_root / active_plan_dir.name
463578
if archived_plan_dir.exists():
464579
raise SystemExit(f"Archived plan directory already exists: {archived_plan_dir}")
465580
archived_plan_dir.parent.mkdir(parents=True, exist_ok=True)

tests/test_orchestration_execution_context.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,42 @@ def test_build_review_package_contains_only_bounded_task_diff_and_evidence(tmp_p
555555
assert ".work-bundle/knowledge/notes" not in package
556556

557557

558+
def test_build_review_package_resolves_git_refs_in_bound_execution_repository(
559+
tmp_path: Path,
560+
) -> None:
561+
root, _, task = workspace(tmp_path)
562+
execution_root = tmp_path / "execution-repository"
563+
execution_root.mkdir()
564+
git(execution_root, "init", "-q", "-b", "main")
565+
git(execution_root, "config", "user.email", "test@example.com")
566+
git(execution_root, "config", "user.name", "Test")
567+
source = execution_root / WRITE_SCOPE_FILE
568+
source.parent.mkdir(parents=True)
569+
source.write_text("def compile_task():\n return 'old'\n", encoding="utf-8")
570+
git(execution_root, "add", ".")
571+
git(execution_root, "commit", "-qm", "base")
572+
base = git(execution_root, "rev-parse", "HEAD")
573+
574+
_set_process_validation(task, PASSING_PROCESS)
575+
brief = _compiled_brief(root, task)
576+
_bind_task_execution(root, brief, execution_root=execution_root)
577+
source.write_text("def compile_task():\n return 'new'\n", encoding="utf-8")
578+
git(execution_root, "add", WRITE_SCOPE_FILE)
579+
git(execution_root, "commit", "-qm", "head")
580+
head = git(execution_root, "rev-parse", "HEAD")
581+
handoff = _handoff_for_command(root, PASSING_PROCESS)
582+
583+
target = build_review_package(
584+
args(root, task, handoff=str(handoff), base=base, head=head)
585+
)
586+
package = target.read_text(encoding="utf-8")
587+
588+
assert target == root / ".work-bundle/runtime/execution/plan-001/task-004/review-package.md"
589+
assert f"Base: {base}" in package
590+
assert f"Head: {head}" in package
591+
assert "return 'new'" in package
592+
593+
558594
def test_build_review_package_includes_tracked_and_untracked_worktree_changes(tmp_path: Path) -> None:
559595
root, _, task = workspace(tmp_path)
560596
source = root / WRITE_SCOPE_FILE

0 commit comments

Comments
 (0)