From 7d69854360cc34ef493987d764e89eebad9f3069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=8D=87=E9=BE=99?= Date: Wed, 26 Aug 2026 15:34:30 +0800 Subject: [PATCH 1/5] fix(orchestration): diff bound execution repository --- scripts/orchestration/execution_context.py | 8 +++-- tests/test_orchestration_execution_context.py | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/scripts/orchestration/execution_context.py b/scripts/orchestration/execution_context.py index 89986e2..2f18386 100644 --- a/scripts/orchestration/execution_context.py +++ b/scripts/orchestration/execution_context.py @@ -1548,10 +1548,14 @@ def build_review_package(args: argparse.Namespace) -> Path: handoff, _ = _read_structured(handoff_path) validated = validate_executor_result_for_task(handoff, task, observe=True, **_observation_kwargs(args)) knowledge_disposition = validated["knowledge_disposition"] + binding = load_task_execution_binding(root, plan_id, task_id) + execution_root = Path(str(binding["execution_path"])).resolve() - base = _resolve_commit(root, str(args.base)) + base = _resolve_commit(execution_root, str(args.base)) write_paths = [str(path) for path in _as_list((task.get("files") or {}).get("write"))] - head, diff, name_status, out_of_scope = _review_diff(root, base, str(args.head), write_paths) + head, diff, name_status, out_of_scope = _review_diff( + execution_root, base, str(args.head), write_paths + ) if len(diff.encode("utf-8")) > MAX_DIFF_BYTES or diff.count("\n") > MAX_DIFF_LINES: oversized = ", ".join( sorted({path for line in name_status for path in _paths_from_name_status(line)}) diff --git a/tests/test_orchestration_execution_context.py b/tests/test_orchestration_execution_context.py index 0a1a27b..fadcd7b 100644 --- a/tests/test_orchestration_execution_context.py +++ b/tests/test_orchestration_execution_context.py @@ -555,6 +555,42 @@ def test_build_review_package_contains_only_bounded_task_diff_and_evidence(tmp_p assert ".work-bundle/knowledge/notes" not in package +def test_build_review_package_resolves_git_refs_in_bound_execution_repository( + tmp_path: Path, +) -> None: + root, _, task = workspace(tmp_path) + execution_root = tmp_path / "execution-repository" + execution_root.mkdir() + git(execution_root, "init", "-q", "-b", "main") + git(execution_root, "config", "user.email", "test@example.com") + git(execution_root, "config", "user.name", "Test") + source = execution_root / WRITE_SCOPE_FILE + source.parent.mkdir(parents=True) + source.write_text("def compile_task():\n return 'old'\n", encoding="utf-8") + git(execution_root, "add", ".") + git(execution_root, "commit", "-qm", "base") + base = git(execution_root, "rev-parse", "HEAD") + + _set_process_validation(task, PASSING_PROCESS) + brief = _compiled_brief(root, task) + _bind_task_execution(root, brief, execution_root=execution_root) + source.write_text("def compile_task():\n return 'new'\n", encoding="utf-8") + git(execution_root, "add", WRITE_SCOPE_FILE) + git(execution_root, "commit", "-qm", "head") + head = git(execution_root, "rev-parse", "HEAD") + handoff = _handoff_for_command(root, PASSING_PROCESS) + + target = build_review_package( + args(root, task, handoff=str(handoff), base=base, head=head) + ) + package = target.read_text(encoding="utf-8") + + assert target == root / ".work-bundle/runtime/execution/plan-001/task-004/review-package.md" + assert f"Base: {base}" in package + assert f"Head: {head}" in package + assert "return 'new'" in package + + def test_build_review_package_includes_tracked_and_untracked_worktree_changes(tmp_path: Path) -> None: root, _, task = workspace(tmp_path) source = root / WRITE_SCOPE_FILE From e89333f386b55fb257abb5f33721ac38cfd2375f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=8D=87=E9=BE=99?= Date: Wed, 26 Aug 2026 19:16:01 +0800 Subject: [PATCH 2/5] fix(orchestration): bind plan acceptance to repository --- scripts/orchestration/plans.py | 37 ++++++++++++++++++- .../test_orchestration_workflow_contracts.py | 24 +++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/scripts/orchestration/plans.py b/scripts/orchestration/plans.py index b542238..aaf50fe 100644 --- a/scripts/orchestration/plans.py +++ b/scripts/orchestration/plans.py @@ -185,6 +185,17 @@ def _handoff_recorded_identities(handoff: dict[str, object]) -> list[str]: def _verified_handoff_tree(root: Path, handoff: dict[str, object]) -> str | None: + repositories = handoff.get("repository") if isinstance(handoff.get("repository"), list) else [] + for repository in repositories: + if not isinstance(repository, dict): + continue + recorded_root = str(repository.get("root") or "").strip() + metadata = repository.get("metadata") if isinstance(repository.get("metadata"), dict) else {} + identity = str(metadata.get("actual_commit") or "").strip() + if recorded_root and identity: + tree = _git_tree_id(Path(recorded_root).expanduser().resolve(), identity) + if tree: + return tree for identity in _handoff_recorded_identities(handoff): tree = _git_tree_id(root, identity) if tree: @@ -192,6 +203,28 @@ def _verified_handoff_tree(root: Path, handoff: dict[str, object]) -> str | None return None +def _material_repository_root( + args: argparse.Namespace, + validated: list[tuple[dict[str, object], dict[str, object]]], +) -> Path: + roots: set[Path] = set() + for handoff, brief in validated: + if not _handoff_has_material_changes(handoff, brief): + continue + repositories = handoff.get("repository") if isinstance(handoff.get("repository"), list) else [] + for repository in repositories: + if not isinstance(repository, dict): + continue + recorded = str(repository.get("root") or "").strip() + if recorded: + roots.add(Path(recorded).expanduser().resolve()) + if len(roots) > 1: + raise SystemExit("acceptance-blocked: final plan repository is ambiguous") + if roots: + return next(iter(roots)) + return project_root(args) + + def _acceptance_result_detail(results: set[str]) -> str: if not results: return "missing" @@ -280,7 +313,7 @@ def _assert_archive_plan_acceptance( commands = _declared_integration_commands(body) if not commands: return - git_root = project_root(args) + git_root = _material_repository_root(args, validated) terminal_tree = _git_tree_id(git_root, "HEAD") material = [pair for pair in validated if _handoff_has_material_changes(*pair)] for command in commands: @@ -309,7 +342,7 @@ def _assert_archive_plan_acceptance( raise SystemExit( f"acceptance-blocked: declared plan-level acceptance {command} is {_acceptance_result_detail(judged)}" ) - workspace = _resolve_final_plan_workspace(args) + workspace = git_root if material else _resolve_final_plan_workspace(args) for command in commands: _assert_archive_command_state_neutral(command, workspace) diff --git a/tests/test_orchestration_workflow_contracts.py b/tests/test_orchestration_workflow_contracts.py index 5436390..0ae4732 100644 --- a/tests/test_orchestration_workflow_contracts.py +++ b/tests/test_orchestration_workflow_contracts.py @@ -19,6 +19,7 @@ validate_executor_result_for_task, ) from handoffs import cmd_write_handoff, index_handoffs +from plans import _verified_handoff_tree def read(path: str) -> str: @@ -94,6 +95,28 @@ def test_handoff_helper_indexes_sparse_executor_result(tmp_path: Path) -> None: assert row["path"].endswith("handoff-exec-20990101-001-task-result.yaml") +def test_handoff_tree_resolves_recorded_repository_instead_of_control_root(tmp_path: Path) -> None: + from test_orchestration_execution_context import git + + control_root = tmp_path / "control" + execution_root = tmp_path / "execution-flow" + control_root.mkdir() + execution_root.mkdir() + git(execution_root, "init", "-q") + git(execution_root, "config", "user.email", "test@example.com") + git(execution_root, "config", "user.name", "Test") + (execution_root / "feature.ts").write_text("export const ready = true;\n", encoding="utf-8") + git(execution_root, "add", ".") + git(execution_root, "commit", "-qm", "feature") + head = git(execution_root, "rev-parse", "HEAD").strip() + tree = git(execution_root, "rev-parse", "HEAD^{tree}").strip() + handoff = { + "repository": [{"root": str(execution_root), "metadata": {"actual_commit": head}}], + } + + assert _verified_handoff_tree(control_root, handoff) == tree + + def test_write_handoff_fills_missing_task_plan_from_authorized_args(tmp_path: Path) -> None: content = tmp_path / "handoff-content.txt" content.write_text( @@ -1354,4 +1377,3 @@ def test_dev_create_task_plan_tests_omit_heavy_orchestration_requirements() -> N tests = read("tests/test_dev_skill_contracts.py") assert "dev-create-task-plan" in tests assert ".work-bundle/runtime/dev-plans/" in tests - From 10d502b00a0a86cc9d11fddf2a2729c37f5dccbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=8D=87=E9=BE=99?= Date: Wed, 26 Aug 2026 19:20:18 +0800 Subject: [PATCH 3/5] fix(orchestration): select terminal acceptance repository --- scripts/orchestration/plans.py | 58 ++++++++++++++++--- .../test_orchestration_workflow_contracts.py | 45 +++++++++++++- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/scripts/orchestration/plans.py b/scripts/orchestration/plans.py index aaf50fe..bb15ac0 100644 --- a/scripts/orchestration/plans.py +++ b/scripts/orchestration/plans.py @@ -164,6 +164,16 @@ def _git_tree_id(root: Path, spec: str) -> str | None: return result.stdout.strip() or None +def _git_is_ancestor(root: Path, ancestor: str, descendant: str) -> bool: + result = subprocess.run( + ["git", "-C", str(root), "merge-base", "--is-ancestor", ancestor, descendant], + capture_output=True, + text=True, + check=False, + ) + return result.returncode == 0 + + def _handoff_recorded_identities(handoff: dict[str, object]) -> list[str]: identities: list[str] = [] review = handoff.get("acceptance_review") if isinstance(handoff.get("acceptance_review"), dict) else {} @@ -206,8 +216,9 @@ def _verified_handoff_tree(root: Path, handoff: dict[str, object]) -> str | None def _material_repository_root( args: argparse.Namespace, validated: list[tuple[dict[str, object], dict[str, object]]], + commands: list[str], ) -> Path: - roots: set[Path] = set() + entries: list[tuple[Path, str]] = [] for handoff, brief in validated: if not _handoff_has_material_changes(handoff, brief): continue @@ -216,13 +227,44 @@ def _material_repository_root( if not isinstance(repository, dict): continue recorded = str(repository.get("root") or "").strip() - if recorded: - roots.add(Path(recorded).expanduser().resolve()) - if len(roots) > 1: + metadata = repository.get("metadata") if isinstance(repository.get("metadata"), dict) else {} + identity = str(metadata.get("actual_commit") or "").strip() + if recorded and identity: + entries.append((Path(recorded).expanduser().resolve(), identity)) + if not entries: + return project_root(args) + acceptance_entries: list[tuple[Path, str]] = [] + for handoff, _brief in validated: + if not any(_handoff_command_result(handoff, command) == "passed" for command in commands): + continue + repositories = handoff.get("repository") if isinstance(handoff.get("repository"), list) else [] + for repository in repositories: + if not isinstance(repository, dict): + continue + recorded = str(repository.get("root") or "").strip() + metadata = repository.get("metadata") if isinstance(repository.get("metadata"), dict) else {} + identity = str(metadata.get("actual_commit") or "").strip() + if recorded and identity: + acceptance_entries.append((Path(recorded).expanduser().resolve(), identity)) + fresh_acceptance_roots = { + root + for root, identity in acceptance_entries + if _git_tree_id(root, "HEAD") == _git_tree_id(root, identity) + } + if len(fresh_acceptance_roots) == 1: + return next(iter(fresh_acceptance_roots)) + terminal: list[tuple[Path, str]] = [] + identities = {identity for _root, identity in entries} + for root, identity in entries: + if _git_tree_id(root, identity) and all(_git_is_ancestor(root, other, identity) for other in identities): + terminal.append((root, identity)) + terminal_identities = {identity for _root, identity in terminal} + if len(terminal_identities) == 1: + identity = next(iter(terminal_identities)) + return next(root for root, candidate in terminal if candidate == identity) + if len({root for root, _identity in entries}) > 1: raise SystemExit("acceptance-blocked: final plan repository is ambiguous") - if roots: - return next(iter(roots)) - return project_root(args) + return entries[0][0] def _acceptance_result_detail(results: set[str]) -> str: @@ -313,7 +355,7 @@ def _assert_archive_plan_acceptance( commands = _declared_integration_commands(body) if not commands: return - git_root = _material_repository_root(args, validated) + git_root = _material_repository_root(args, validated, commands) terminal_tree = _git_tree_id(git_root, "HEAD") material = [pair for pair in validated if _handoff_has_material_changes(*pair)] for command in commands: diff --git a/tests/test_orchestration_workflow_contracts.py b/tests/test_orchestration_workflow_contracts.py index 0ae4732..a9a9729 100644 --- a/tests/test_orchestration_workflow_contracts.py +++ b/tests/test_orchestration_workflow_contracts.py @@ -19,7 +19,7 @@ validate_executor_result_for_task, ) from handoffs import cmd_write_handoff, index_handoffs -from plans import _verified_handoff_tree +from plans import _material_repository_root, _verified_handoff_tree def read(path: str) -> str: @@ -117,6 +117,49 @@ def test_handoff_tree_resolves_recorded_repository_instead_of_control_root(tmp_p assert _verified_handoff_tree(control_root, handoff) == tree +def test_material_repository_prefers_fresh_plan_acceptance_root(tmp_path: Path) -> None: + from test_orchestration_execution_context import git + + control_root = tmp_path / "control" + earlier_root = tmp_path / "earlier" + accepted_root = tmp_path / "accepted" + control_root.mkdir() + for root, content in ((earlier_root, "old\n"), (accepted_root, "accepted\n")): + root.mkdir() + git(root, "init", "-q") + git(root, "config", "user.email", "test@example.com") + git(root, "config", "user.name", "Test") + (root / "feature.ts").write_text(content, encoding="utf-8") + git(root, "add", ".") + git(root, "commit", "-qm", "feature") + + command = "pnpm run ci" + earlier_head = git(earlier_root, "rev-parse", "HEAD").strip() + accepted_head = git(accepted_root, "rev-parse", "HEAD").strip() + validated = [ + ( + { + "changes": {"files": [{"path": "feature.ts", "action": "modified"}]}, + "repository": [{"root": str(earlier_root), "metadata": {"actual_commit": earlier_head}}], + "validation": {"commands": []}, + }, + {"files": {"write": ["feature.ts"]}}, + ), + ( + { + "changes": {"files": [{"path": "feature.ts", "action": "modified"}]}, + "repository": [{"root": str(accepted_root), "metadata": {"actual_commit": accepted_head}}], + "validation": {"commands": [{"command": command, "result": "passed"}]}, + }, + {"files": {"write": ["feature.ts"]}}, + ), + ] + + assert _material_repository_root( + argparse.Namespace(project_root=str(control_root)), validated, [command] + ) == accepted_root.resolve() + + def test_write_handoff_fills_missing_task_plan_from_authorized_args(tmp_path: Path) -> None: content = tmp_path / "handoff-content.txt" content.write_text( From bd2008956634aa2dea8a82c88516460683d77e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=8D=87=E9=BE=99?= Date: Wed, 26 Aug 2026 19:34:31 +0800 Subject: [PATCH 4/5] fix(orchestration): require terminal acceptance evidence --- scripts/orchestration/plans.py | 80 ++++++++----- .../test_orchestration_workflow_contracts.py | 106 +++++++++++++++++- 2 files changed, 151 insertions(+), 35 deletions(-) diff --git a/scripts/orchestration/plans.py b/scripts/orchestration/plans.py index bb15ac0..cc9f195 100644 --- a/scripts/orchestration/plans.py +++ b/scripts/orchestration/plans.py @@ -164,16 +164,6 @@ def _git_tree_id(root: Path, spec: str) -> str | None: return result.stdout.strip() or None -def _git_is_ancestor(root: Path, ancestor: str, descendant: str) -> bool: - result = subprocess.run( - ["git", "-C", str(root), "merge-base", "--is-ancestor", ancestor, descendant], - capture_output=True, - text=True, - check=False, - ) - return result.returncode == 0 - - def _handoff_recorded_identities(handoff: dict[str, object]) -> list[str]: identities: list[str] = [] review = handoff.get("acceptance_review") if isinstance(handoff.get("acceptance_review"), dict) else {} @@ -215,6 +205,7 @@ def _verified_handoff_tree(root: Path, handoff: dict[str, object]) -> str | None def _material_repository_root( args: argparse.Namespace, + plan_id: str, validated: list[tuple[dict[str, object], dict[str, object]]], commands: list[str], ) -> Path: @@ -233,10 +224,34 @@ def _material_repository_root( entries.append((Path(recorded).expanduser().resolve(), identity)) if not entries: return project_root(args) + roots = {root for root, _identity in entries} + if len(roots) == 1: + return next(iter(roots)) + task_order = { + str(row.get("id") or ""): index + for index, row in enumerate( + row + for row in index_plans(args) + if row.get("type") == "task" and row.get("plan_id") == plan_id + ) + } + material_ranks: list[int] = [] + for handoff, brief in validated: + if not _handoff_has_material_changes(handoff, brief): + continue + task_id = str(brief.get("task_id") or "") + if task_id not in task_order: + raise SystemExit("acceptance-blocked: final plan task order is unavailable") + material_ranks.append(task_order[task_id]) + terminal_material_rank = max(material_ranks) if material_ranks else -1 acceptance_entries: list[tuple[Path, str]] = [] - for handoff, _brief in validated: + for handoff, brief in validated: if not any(_handoff_command_result(handoff, command) == "passed" for command in commands): continue + task_id = str(brief.get("task_id") or "") + rank = task_order.get(task_id) + if rank is None or rank < terminal_material_rank: + continue repositories = handoff.get("repository") if isinstance(handoff.get("repository"), list) else [] for repository in repositories: if not isinstance(repository, dict): @@ -246,25 +261,15 @@ def _material_repository_root( identity = str(metadata.get("actual_commit") or "").strip() if recorded and identity: acceptance_entries.append((Path(recorded).expanduser().resolve(), identity)) - fresh_acceptance_roots = { - root - for root, identity in acceptance_entries - if _git_tree_id(root, "HEAD") == _git_tree_id(root, identity) - } + fresh_acceptance_roots: set[Path] = set() + for root, identity in acceptance_entries: + head_tree = _git_tree_id(root, "HEAD") + recorded_tree = _git_tree_id(root, identity) + if head_tree is not None and recorded_tree is not None and head_tree == recorded_tree: + fresh_acceptance_roots.add(root) if len(fresh_acceptance_roots) == 1: return next(iter(fresh_acceptance_roots)) - terminal: list[tuple[Path, str]] = [] - identities = {identity for _root, identity in entries} - for root, identity in entries: - if _git_tree_id(root, identity) and all(_git_is_ancestor(root, other, identity) for other in identities): - terminal.append((root, identity)) - terminal_identities = {identity for _root, identity in terminal} - if len(terminal_identities) == 1: - identity = next(iter(terminal_identities)) - return next(root for root, candidate in terminal if candidate == identity) - if len({root for root, _identity in entries}) > 1: - raise SystemExit("acceptance-blocked: final plan repository is ambiguous") - return entries[0][0] + raise SystemExit("acceptance-blocked: final plan repository is ambiguous") def _acceptance_result_detail(results: set[str]) -> str: @@ -355,7 +360,7 @@ def _assert_archive_plan_acceptance( commands = _declared_integration_commands(body) if not commands: return - git_root = _material_repository_root(args, validated, commands) + git_root = _material_repository_root(args, plan_id, validated, commands) terminal_tree = _git_tree_id(git_root, "HEAD") material = [pair for pair in validated if _handoff_has_material_changes(*pair)] for command in commands: @@ -532,9 +537,22 @@ def cmd_archive_plan(args: argparse.Namespace) -> None: replace_front_matter_value(root_path, "status", "Completed") moved.append(move_to_archive(root_path, active_root, archived_root)) - active_plan_dir = active_root / args.id + sibling_plan_dir = root_path.with_suffix("") + indexed_active_dirs = { + active_root / artifact_path_from_row(row, args).relative_to(active_root).parts[0] + for row in rows + if row.get("type") == "task" + and row.get("plan_id") == args.id + and is_relative_to(artifact_path_from_row(row, args), active_root) + } + if sibling_plan_dir.is_dir() and is_relative_to(sibling_plan_dir, active_root): + active_plan_dir = sibling_plan_dir + elif len(indexed_active_dirs) == 1: + active_plan_dir = next(iter(indexed_active_dirs)) + else: + active_plan_dir = active_root / args.id if active_plan_dir.exists(): - archived_plan_dir = archived_root / args.id + archived_plan_dir = archived_root / active_plan_dir.name if archived_plan_dir.exists(): raise SystemExit(f"Archived plan directory already exists: {archived_plan_dir}") archived_plan_dir.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/test_orchestration_workflow_contracts.py b/tests/test_orchestration_workflow_contracts.py index a9a9729..189c86c 100644 --- a/tests/test_orchestration_workflow_contracts.py +++ b/tests/test_orchestration_workflow_contracts.py @@ -117,7 +117,9 @@ def test_handoff_tree_resolves_recorded_repository_instead_of_control_root(tmp_p assert _verified_handoff_tree(control_root, handoff) == tree -def test_material_repository_prefers_fresh_plan_acceptance_root(tmp_path: Path) -> None: +def test_material_repository_prefers_fresh_terminal_plan_acceptance_root( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: from test_orchestration_execution_context import git control_root = tmp_path / "control" @@ -143,7 +145,7 @@ def test_material_repository_prefers_fresh_plan_acceptance_root(tmp_path: Path) "repository": [{"root": str(earlier_root), "metadata": {"actual_commit": earlier_head}}], "validation": {"commands": []}, }, - {"files": {"write": ["feature.ts"]}}, + {"task_id": "task-001", "files": {"write": ["feature.ts"]}}, ), ( { @@ -151,15 +153,73 @@ def test_material_repository_prefers_fresh_plan_acceptance_root(tmp_path: Path) "repository": [{"root": str(accepted_root), "metadata": {"actual_commit": accepted_head}}], "validation": {"commands": [{"command": command, "result": "passed"}]}, }, - {"files": {"write": ["feature.ts"]}}, + {"task_id": "task-002", "files": {"write": ["feature.ts"]}}, ), ] + monkeypatch.setattr( + "plans.index_plans", + lambda _args: [ + {"type": "task", "plan_id": "plan-001", "id": "task-001"}, + {"type": "task", "plan_id": "plan-001", "id": "task-002"}, + ], + ) assert _material_repository_root( - argparse.Namespace(project_root=str(control_root)), validated, [command] + argparse.Namespace(project_root=str(control_root)), "plan-001", validated, [command] ) == accepted_root.resolve() +def test_material_repository_rejects_fresh_acceptance_before_later_material_task( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from test_orchestration_execution_context import git + + earlier_root = tmp_path / "earlier" + later_root = tmp_path / "later" + for root in (earlier_root, later_root): + root.mkdir() + git(root, "init", "-q") + git(root, "config", "user.email", "test@example.com") + git(root, "config", "user.name", "Test") + (root / "feature.ts").write_text(f"{root.name}\n", encoding="utf-8") + git(root, "add", ".") + git(root, "commit", "-qm", "feature") + + command = "pnpm run ci" + earlier_head = git(earlier_root, "rev-parse", "HEAD").strip() + later_head = git(later_root, "rev-parse", "HEAD").strip() + validated = [ + ( + { + "changes": {"files": [{"path": "feature.ts", "action": "modified"}]}, + "repository": [{"root": str(earlier_root), "metadata": {"actual_commit": earlier_head}}], + "validation": {"commands": [{"command": command, "result": "passed"}]}, + }, + {"task_id": "task-001", "files": {"write": ["feature.ts"]}}, + ), + ( + { + "changes": {"files": [{"path": "feature.ts", "action": "modified"}]}, + "repository": [{"root": str(later_root), "metadata": {"actual_commit": later_head}}], + "validation": {"commands": []}, + }, + {"task_id": "task-002", "files": {"write": ["feature.ts"]}}, + ), + ] + monkeypatch.setattr( + "plans.index_plans", + lambda _args: [ + {"type": "task", "plan_id": "plan-001", "id": "task-001"}, + {"type": "task", "plan_id": "plan-001", "id": "task-002"}, + ], + ) + + with pytest.raises(SystemExit, match="acceptance-blocked: final plan repository is ambiguous"): + _material_repository_root( + argparse.Namespace(project_root=str(tmp_path)), "plan-001", validated, [command] + ) + + def test_write_handoff_fills_missing_task_plan_from_authorized_args(tmp_path: Path) -> None: content = tmp_path / "handoff-content.txt" content.write_text( @@ -1102,6 +1162,44 @@ def test_fresh_plan_acceptance_rerun_after_later_task_allows_archive(tmp_path: P assert (root / ".work-bundle/orchestration/plan/archived/compiler-plan.md").is_file() +def test_archive_moves_plan_directory_named_for_root_artifact(tmp_path: Path) -> None: + from plans import cmd_archive_plan + from test_orchestration_execution_context import workspace + + root, _, _ = workspace(tmp_path) + _append_plan_knowledge(root, closure_return="missing") + active = root / ".work-bundle/orchestration/plan/active" + (active / "compiler-plan.md").rename(active / "plan-001-feature.md") + (active / "plan-001").rename(active / "plan-001-feature") + + cmd_archive_plan(argparse.Namespace(project_root=str(root), id="plan-001")) + + archived = root / ".work-bundle/orchestration/plan/archived" + assert (archived / "plan-001-feature.md").is_file() + assert (archived / "plan-001-feature").is_dir() + assert not (active / "plan-001-feature").exists() + + +def test_archive_reconciles_archived_root_with_active_plan_directory(tmp_path: Path) -> None: + from plans import cmd_archive_plan + from test_orchestration_execution_context import workspace + + root, _, _ = workspace(tmp_path) + _append_plan_knowledge(root, closure_return="missing") + plan_root = root / ".work-bundle/orchestration/plan" + active = plan_root / "active" + archived = plan_root / "archived" + archived.mkdir(exist_ok=True) + (active / "compiler-plan.md").rename(archived / "plan-001-feature.md") + (active / "plan-001").rename(active / "plan-001-feature") + + cmd_archive_plan(argparse.Namespace(project_root=str(root), id="plan-001")) + + assert (archived / "plan-001-feature.md").is_file() + assert (archived / "plan-001-feature").is_dir() + assert not (active / "plan-001-feature").exists() + + def test_same_day_out_of_id_order_stale_plan_acceptance_blocks_archive(tmp_path: Path) -> None: from plans import cmd_archive_plan from test_orchestration_execution_context import WRITE_SCOPE_FILE, workspace From c8a95e0a967b614146d4efc1dcfefe8a3dab49ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=8D=87=E9=BE=99?= Date: Wed, 26 Aug 2026 20:27:53 +0800 Subject: [PATCH 5/5] fix(orchestration): fail closed on terminal provenance --- scripts/orchestration/plans.py | 48 +++++++++++----- .../test_orchestration_workflow_contracts.py | 55 ++++++++++++++----- 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/scripts/orchestration/plans.py b/scripts/orchestration/plans.py index cc9f195..a585f8b 100644 --- a/scripts/orchestration/plans.py +++ b/scripts/orchestration/plans.py @@ -203,6 +203,26 @@ def _verified_handoff_tree(root: Path, handoff: dict[str, object]) -> str | None return None +def _plan_task_order(args: argparse.Namespace, plan_id: str) -> dict[str, int]: + order: dict[str, int] = {} + for row in index_plans(args): + if row.get("type") != "task" or row.get("plan_id") != plan_id: + continue + front_matter, _body = read_front_matter(artifact_path_from_row(row, args)) + task_id = str(front_matter.get("id") or row.get("id") or "") + value = front_matter.get("order") + rank: int | None = None + if isinstance(value, int) and not isinstance(value, bool): + rank = value + elif isinstance(value, str) and re.fullmatch(r"[1-9]\d*", value.strip()): + rank = int(value) + if task_id and rank is not None: + if rank in order.values(): + raise SystemExit("acceptance-blocked: final plan task order is ambiguous") + order[task_id] = rank + return order + + def _material_repository_root( args: argparse.Namespace, plan_id: str, @@ -210,9 +230,11 @@ def _material_repository_root( commands: list[str], ) -> Path: entries: list[tuple[Path, str]] = [] - for handoff, brief in validated: - if not _handoff_has_material_changes(handoff, brief): - continue + material = [pair for pair in validated if _handoff_has_material_changes(*pair)] + if not material: + return project_root(args) + for handoff, _brief in material: + handoff_has_provenance = False repositories = handoff.get("repository") if isinstance(handoff.get("repository"), list) else [] for repository in repositories: if not isinstance(repository, dict): @@ -222,19 +244,19 @@ def _material_repository_root( identity = str(metadata.get("actual_commit") or "").strip() if recorded and identity: entries.append((Path(recorded).expanduser().resolve(), identity)) - if not entries: - return project_root(args) + handoff_has_provenance = True + if not handoff_has_provenance: + try: + fallback = _resolve_final_plan_workspace(args) + except SystemExit as error: + raise SystemExit( + "acceptance-blocked: material handoff repository provenance is unavailable" + ) from error + entries.append((fallback, "HEAD")) roots = {root for root, _identity in entries} if len(roots) == 1: return next(iter(roots)) - task_order = { - str(row.get("id") or ""): index - for index, row in enumerate( - row - for row in index_plans(args) - if row.get("type") == "task" and row.get("plan_id") == plan_id - ) - } + task_order = _plan_task_order(args, plan_id) material_ranks: list[int] = [] for handoff, brief in validated: if not _handoff_has_material_changes(handoff, brief): diff --git a/tests/test_orchestration_workflow_contracts.py b/tests/test_orchestration_workflow_contracts.py index 189c86c..41a69c9 100644 --- a/tests/test_orchestration_workflow_contracts.py +++ b/tests/test_orchestration_workflow_contracts.py @@ -156,13 +156,7 @@ def test_material_repository_prefers_fresh_terminal_plan_acceptance_root( {"task_id": "task-002", "files": {"write": ["feature.ts"]}}, ), ] - monkeypatch.setattr( - "plans.index_plans", - lambda _args: [ - {"type": "task", "plan_id": "plan-001", "id": "task-001"}, - {"type": "task", "plan_id": "plan-001", "id": "task-002"}, - ], - ) + monkeypatch.setattr("plans._plan_task_order", lambda _args, _plan_id: {"task-001": 1, "task-002": 2}) assert _material_repository_root( argparse.Namespace(project_root=str(control_root)), "plan-001", validated, [command] @@ -195,7 +189,7 @@ def test_material_repository_rejects_fresh_acceptance_before_later_material_task "repository": [{"root": str(earlier_root), "metadata": {"actual_commit": earlier_head}}], "validation": {"commands": [{"command": command, "result": "passed"}]}, }, - {"task_id": "task-001", "files": {"write": ["feature.ts"]}}, + {"task_id": "task-010", "files": {"write": ["feature.ts"]}}, ), ( { @@ -206,13 +200,7 @@ def test_material_repository_rejects_fresh_acceptance_before_later_material_task {"task_id": "task-002", "files": {"write": ["feature.ts"]}}, ), ] - monkeypatch.setattr( - "plans.index_plans", - lambda _args: [ - {"type": "task", "plan_id": "plan-001", "id": "task-001"}, - {"type": "task", "plan_id": "plan-001", "id": "task-002"}, - ], - ) + monkeypatch.setattr("plans._plan_task_order", lambda _args, _plan_id: {"task-010": 1, "task-002": 2}) with pytest.raises(SystemExit, match="acceptance-blocked: final plan repository is ambiguous"): _material_repository_root( @@ -220,6 +208,43 @@ def test_material_repository_rejects_fresh_acceptance_before_later_material_task ) +def test_material_repository_rejects_material_handoff_without_repository_provenance(tmp_path: Path) -> None: + command = "pnpm run ci" + first = tmp_path / "first" + second = tmp_path / "second" + first.mkdir() + second.mkdir() + metadata = tmp_path / ".work-bundle/project.yaml" + metadata.parent.mkdir() + metadata.write_text( + "metadata_version: 3\n" + "workspace_root: " + str(tmp_path) + "\n" + "workspace_mode: multi-repository\n" + "source_repositories:\n" + " first:\n" + " project_root: " + str(first) + "\n" + " second:\n" + " project_root: " + str(second) + "\n", + encoding="utf-8", + ) + validated = [ + ( + { + "changes": {"files": [{"path": "feature.ts", "action": "modified"}]}, + "validation": {"commands": [{"command": command, "result": "passed"}]}, + }, + {"task_id": "task-001", "files": {"write": ["feature.ts"]}}, + ), + ] + + with pytest.raises( + SystemExit, match="acceptance-blocked: material handoff repository provenance is unavailable" + ): + _material_repository_root( + argparse.Namespace(project_root=str(tmp_path)), "plan-001", validated, [command] + ) + + def test_write_handoff_fills_missing_task_plan_from_authorized_args(tmp_path: Path) -> None: content = tmp_path / "handoff-content.txt" content.write_text(