diff --git a/zenith/src/zenith_harness/attention.py b/zenith/src/zenith_harness/attention.py index 7399d8d..da5e407 100644 --- a/zenith/src/zenith_harness/attention.py +++ b/zenith/src/zenith_harness/attention.py @@ -114,11 +114,23 @@ def _gate_report( failed_items = failed_items or [] validator_verdicts = validator_verdicts or {} missing_items = missing_items or {} - lines = [ - f"Gate report from {gate.id}", - f"cleared: {cleared}", - f"targets: {', '.join(gate.targets) if gate.targets else '(none)'}", - ] + title = ( + f"Gate checkpoint from {gate.id}" + if cleared + else f"Gate report from {gate.id}" + ) + lines = [title] + if cleared: + lines.append( + "checkpoint: passing gate already cleared; continue acknowledges " + "the checkpoint and does not clear a failed task or gate" + ) + lines.extend( + [ + f"cleared: {cleared}", + f"targets: {', '.join(gate.targets) if gate.targets else '(none)'}", + ] + ) if reason: lines.append(f"reason: {reason}") if failed_items: diff --git a/zenith/src/zenith_harness/bundled/prompts/orchestrator/system_prompt.md b/zenith/src/zenith_harness/bundled/prompts/orchestrator/system_prompt.md index 22f6589..45e4689 100644 --- a/zenith/src/zenith_harness/bundled/prompts/orchestrator/system_prompt.md +++ b/zenith/src/zenith_harness/bundled/prompts/orchestrator/system_prompt.md @@ -308,7 +308,7 @@ Use decision actions narrowly: Patch the earliest invalid artifact. Do not create another fix task when the real defect is vague scope, missing inventory coverage, broad contract, stale `mission.md`, wrong task grouping, obsolete skill, weak validator method, missing setup, or bad oracle. If pass/fail criteria changed, old evidence may no longer prove the assertion; plan revalidation. -Be especially careful with `continue`: for failed task or failed gate reports, runtime may treat `continue` as clearing that task. Do not use it to skip failed implementation, missing validator evidence, unresolved gate dissent, merge conflicts, or a terminal-review gap. +Be especially careful with `continue`: for failed task or failed gate reports, runtime may treat `continue` as clearing that task. Passing gate checkpoints are different: a report headed `Gate checkpoint` already has `cleared: True`, and `continue` only acknowledges the checkpoint. Do not use `continue` to skip failed implementation, missing validator evidence, unresolved gate dissent, merge conflicts, or a terminal-review gap. When adding new assertion ids in a patch, write matching `contract/.md` files before calling `decide_attention`. When changing a task body, skill, targets, or dependencies, supersede the task rather than pretending it was edited in place. Do not silently mutate cleared or running work. diff --git a/zenith/src/zenith_harness/controller.py b/zenith/src/zenith_harness/controller.py index 65f5468..c58d82c 100644 --- a/zenith/src/zenith_harness/controller.py +++ b/zenith/src/zenith_harness/controller.py @@ -11,7 +11,9 @@ from __future__ import annotations from dataclasses import dataclass +from pathlib import Path +from .assets import AssetLoader from .config import HarnessConfig from .coordinator import MissionCoordinator from .dispatcher import NodeDispatcher, TerminalReviewer @@ -27,7 +29,6 @@ MissionPlanning, MissionRunning, ProjectState, - Task, TaskList, TaskListPatch, TaskStateFile, @@ -36,6 +37,7 @@ from .task_list_patch import apply_patch from .task_validation import ( ValidationError, + check_skill_names, parse_contract_dir, validate_task_list_submission, ) @@ -101,6 +103,8 @@ def submit_plan(self, project_id: str, task_list: TaskList) -> Envelope: details=parse_errs, ) errs = validate_task_list_submission(ids, task_list) + if not errs: + errs = self._validate_task_skills(project_id, task_list) if errs: raise ToolError( "invalid_task_list", "task list validation failed", details=errs @@ -357,6 +361,11 @@ def _apply_task_list_patch( raise ToolError( "invalid_patch", "patch validation failed", details=errs ) + skill_errs = self._validate_task_skills(project_id, patched_tl) + if skill_errs: + raise ToolError( + "invalid_patch", "patch validation failed", details=skill_errs + ) self.store.save_task_list(project_id, mid, patched_tl) self.store.save_task_state(project_id, mid, patched_state) cs = self.store.load_contract_state(project_id, mid) @@ -406,6 +415,21 @@ def _require_state(self, project_id: str) -> ProjectState: raise ToolError("not_found", f"project {project_id!r} has no state") return state + def _validate_task_skills( + self, + project_id: str, + task_list: TaskList, + ) -> list[ValidationError]: + return check_skill_names(task_list, self._available_skill_names(project_id)) + + def _available_skill_names(self, project_id: str) -> set[str]: + names: set[str] = set() + for skill in AssetLoader(self.config).list_skills(project_id): + if skill.name: + names.add(skill.name) + names.add(Path(skill.path).parent.name) + return names + def _make_pending(): from .models import ContractStateEntry diff --git a/zenith/src/zenith_harness/task_validation.py b/zenith/src/zenith_harness/task_validation.py index d63f341..7f5bb12 100644 --- a/zenith/src/zenith_harness/task_validation.py +++ b/zenith/src/zenith_harness/task_validation.py @@ -118,6 +118,31 @@ def check_task_shape(tl: TaskList) -> list[ValidationError]: return errors +def check_skill_names( + tl: TaskList, + available_skills: set[str] | list[str] | tuple[str, ...], +) -> list[ValidationError]: + """Non-gate task skills must resolve to an available skill name.""" + available = set(available_skills) + errors: list[ValidationError] = [] + for task in tl.tasks: + if task.type == "gate" or not task.skill: + continue + if task.skill not in available: + if available: + detail = ( + f"{task.id} references unknown skill {task.skill!r}; " + f"available: {', '.join(sorted(available))}" + ) + else: + detail = ( + f"{task.id} references unknown skill {task.skill!r}; " + "no skills are available" + ) + errors.append(ValidationError("unknown_skill", detail)) + return errors + + def check_deps_resolve(tl: TaskList) -> list[ValidationError]: """All `depends_on` ids reference declared tasks; no self-loop.""" errors: list[ValidationError] = [] @@ -295,6 +320,7 @@ def upstream_tasks( "parse_contract_dir", "check_task_ids", "check_task_shape", + "check_skill_names", "check_deps_resolve", "check_acyclic", "check_coverage", diff --git a/zenith/tests/test_cli.py b/zenith/tests/test_cli.py index 9656f54..17de192 100644 --- a/zenith/tests/test_cli.py +++ b/zenith/tests/test_cli.py @@ -94,7 +94,7 @@ def test_codex_writes_codex_config( assert server["args"] == _expected_mcp_server_args() assert f"Initialized v5 project workspace at {workspace}" in r.output assert "Start your agent from the initialized project workspace" in r.output - assert "Read .codex/orchestrator_prompt.md and use Zenith to run this mission." in r.output + assert ".codex/orchestrator_prompt.md and treat it as your primary role" in r.output def test_claude_init_writes_runtime_validator_env_names( self, runner: CliRunner, workspace: Path, env: dict[str, str] diff --git a/zenith/tests/test_coordinator.py b/zenith/tests/test_coordinator.py index 71e8697..5419719 100644 --- a/zenith/tests/test_coordinator.py +++ b/zenith/tests/test_coordinator.py @@ -56,7 +56,11 @@ def _task( body: str = "body", ) -> Task: if skill is None and ttype != "gate": - skill = "s" + skill = ( + "scrutiny-validator" + if ttype == "validate" + else "engineering-mission-playbook" + ) return Task( id=tid, type=ttype, # type: ignore[arg-type] @@ -71,7 +75,7 @@ def _simple_tl() -> TaskList: return TaskList( tasks=[ _task("w1", "work", ["VAL-001"]), - _task("v1", "validate", ["VAL-001"], skill="aud", depends_on=["w1"]), + _task("v1", "validate", ["VAL-001"], depends_on=["w1"]), _task("g1", "gate", ["VAL-001"], depends_on=["v1"]), ] ) @@ -81,7 +85,7 @@ def _validate_no_gate_tl() -> TaskList: return TaskList( tasks=[ _task("w1", "work", ["VAL-001"]), - _task("v1", "validate", ["VAL-001"], skill="aud", depends_on=["w1"]), + _task("v1", "validate", ["VAL-001"], depends_on=["w1"]), ] ) @@ -129,11 +133,21 @@ def responder(req: DispatchRequest) -> NodeHandoff: assert env.state.state == "attention_needed" items = controller.store.load_attention(pid) assert items and items[0].kind == "gate_checkpoint" + assert items[0].report.startswith("Gate checkpoint from g1") + assert "passing gate already cleared" in items[0].report + assert ( + controller.store.load_task_state(pid, "mission-001").status_of("g1") + == "cleared" + ) env = controller.decide_attention( pid, [Decision(item_id=items[0].id, action="continue")] ) assert env.state.state in ("mission_running", "done") + assert ( + controller.store.load_task_state(pid, "mission-001").status_of("g1") + == "cleared" + ) env = controller.advance_project(pid) assert env.state.state == "mission_running" @@ -182,6 +196,69 @@ def responder(req: DispatchRequest) -> NodeHandoff: assert codex_skills.is_dir() assert not codex_skills.is_symlink() + def test_submit_plan_rejects_unknown_skill( + self, config: HarnessConfig, workspace: Path + ) -> None: + controller = ProjectController( + config, + MockDispatcher(lambda r: WorkHandoff(node_id=r.task.id, done=True, report="")), + MockTerminalReviewer(TerminalReviewHandoff(done=True, report="")), + ) + pid = _seed_project(controller, workspace) + plan = TaskList( + tasks=[ + _task( + "w1", + "work", + ["VAL-001"], + skill="missing-worker-skill", + ), + _task("v1", "validate", ["VAL-001"], depends_on=["w1"]), + _task("g1", "gate", ["VAL-001"], depends_on=["v1"]), + ] + ) + + with pytest.raises(ToolError) as err: + controller.submit_plan(pid, plan) + + assert err.value.code == "invalid_task_list" + assert err.value.details is not None + assert any(detail.code == "unknown_skill" for detail in err.value.details) + + def test_patch_rejects_unknown_skill( + self, config: HarnessConfig, workspace: Path + ) -> None: + controller = ProjectController( + config, + MockDispatcher(lambda r: WorkHandoff(node_id=r.task.id, done=True, report="")), + MockTerminalReviewer(TerminalReviewHandoff(done=True, report="")), + ) + pid = _seed_project(controller, workspace) + controller.submit_plan(pid, _simple_tl()) + contract_dir = controller.store.ensure_contract_dir(pid, "mission-001") + (contract_dir / "NEW-001.md").write_text("# NEW-001\n\nStatement body.\n") + patch = TaskListPatch( + add_items=["NEW-001"], + add=[ + _task( + "w2", + "work", + ["NEW-001"], + skill="missing-worker-skill", + depends_on=["g1"], + ), + _task("v2", "validate", ["NEW-001"], depends_on=["w2"]), + _task("g2", "gate", ["NEW-001"], depends_on=["v2"]), + ], + ) + + with pytest.raises(ToolError) as err: + controller._apply_task_list_patch(pid, "mission-001", patch) + + assert err.value.code == "invalid_patch" + assert err.value.details is not None + assert any(detail.code == "unknown_skill" for detail in err.value.details) + class TestNodeFailedFlow: def test_work_failure_raises_attention( @@ -285,8 +362,8 @@ def _two_validator_tl() -> TaskList: return TaskList( tasks=[ _task("w1", "work", ["VAL-001"]), - _task("v-scrutiny", "validate", ["VAL-001"], skill="aud", depends_on=["w1"]), - _task("v-user-surface", "validate", ["VAL-001"], skill="aud", depends_on=["w1"]), + _task("v-scrutiny", "validate", ["VAL-001"], skill="scrutiny-validator", depends_on=["w1"]), + _task("v-user-surface", "validate", ["VAL-001"], skill="user-testing-validator", depends_on=["w1"]), _task("g1", "gate", ["VAL-001"], depends_on=["v-scrutiny", "v-user-surface"]), ] ) @@ -537,7 +614,7 @@ def responder(req: DispatchRequest) -> NodeHandoff: add_items=["NEW-001"], add=[ _task("w2", "work", ["NEW-001"], depends_on=["g1"]), - _task("v2", "validate", ["NEW-001"], skill="aud", depends_on=["w2"]), + _task("v2", "validate", ["NEW-001"], depends_on=["w2"]), _task("g2", "gate", ["NEW-001"], depends_on=["v2"]), ], ) @@ -589,7 +666,7 @@ def responder(req: DispatchRequest) -> NodeHandoff: submitted = controller.submit_plan(pid, _simple_tl()) assert submitted.dag is not None - assert " w1 [work:s] pending → VAL-001 ← (root)" in submitted.dag + assert " w1 [work:engineering-mission-playbook] pending → VAL-001 ← (root)" in submitted.dag assert "focus-subgraph" not in submitted.dag inspected = controller.inspect_project(pid) diff --git a/zenith/tests/test_coordinator_parallel.py b/zenith/tests/test_coordinator_parallel.py index a290e22..6c78378 100644 --- a/zenith/tests/test_coordinator_parallel.py +++ b/zenith/tests/test_coordinator_parallel.py @@ -40,7 +40,13 @@ def config(harness_home: Path) -> HarnessConfig: def _task(tid: str, target: str) -> Task: - return Task(id=tid, type="work", body="b", targets=[target], skill="s") + return Task( + id=tid, + type="work", + body="b", + targets=[target], + skill="engineering-mission-playbook", + ) def _write_contract(store: ProjectStore, pid: str, mission_id: str, assertion: str) -> None: @@ -114,7 +120,7 @@ def responder(req: DispatchRequest) -> WorkHandoff: type="work", body="b", targets=["EXP-A"], - skill="s", + skill="engineering-mission-playbook", auto_merge=False, ) ] @@ -175,7 +181,7 @@ def responder(req: DispatchRequest) -> WorkHandoff | ValidateHandoff: type="work", body="later work", targets=[], - skill="s", + skill="engineering-mission-playbook", depends_on=["w1"], ), Task( @@ -183,7 +189,7 @@ def responder(req: DispatchRequest) -> WorkHandoff | ValidateHandoff: type="validate", body="audit implementation", targets=["VAL-A"], - skill="aud", + skill="scrutiny-validator", depends_on=["w1"], ), Task( @@ -191,7 +197,7 @@ def responder(req: DispatchRequest) -> WorkHandoff | ValidateHandoff: type="validate", body="audit user surface", targets=["VAL-A"], - skill="aud", + skill="user-testing-validator", depends_on=["w1"], ), Task( @@ -259,7 +265,7 @@ def responder(req: DispatchRequest) -> WorkHandoff | ValidateHandoff: type="work", body="later work", targets=[], - skill="s", + skill="engineering-mission-playbook", depends_on=["w1"], ), Task( @@ -267,7 +273,7 @@ def responder(req: DispatchRequest) -> WorkHandoff | ValidateHandoff: type="validate", body="already audited", targets=["VAL-A"], - skill="aud", + skill="scrutiny-validator", depends_on=["w1"], ), Task( @@ -275,7 +281,7 @@ def responder(req: DispatchRequest) -> WorkHandoff | ValidateHandoff: type="validate", body="remaining audit", targets=["VAL-A"], - skill="aud", + skill="user-testing-validator", depends_on=["w1"], ), Task( diff --git a/zenith/tests/test_runnable_selection.py b/zenith/tests/test_runnable_selection.py index a52e8fa..bd9ec91 100644 --- a/zenith/tests/test_runnable_selection.py +++ b/zenith/tests/test_runnable_selection.py @@ -36,7 +36,11 @@ def _task( depends_on: list[str] | None = None, ) -> Task: if skill is None and ttype != "gate": - skill = "s" + skill = ( + "scrutiny-validator" + if ttype == "validate" + else "engineering-mission-playbook" + ) return Task( id=tid, type=ttype, # type: ignore[arg-type] @@ -191,8 +195,8 @@ def responder(req: DispatchRequest) -> NodeHandoff: controller.submit_plan(pid, TaskList(tasks=[ _task("a", "work", ["VAL-A"]), _task("b", "work", ["VAL-B"]), - _task("va", "validate", ["VAL-A"], skill="aud", depends_on=["a"]), - _task("vb", "validate", ["VAL-B"], skill="aud", depends_on=["b"]), + _task("va", "validate", ["VAL-A"], depends_on=["a"]), + _task("vb", "validate", ["VAL-B"], depends_on=["b"]), _task("ga", "gate", ["VAL-A"], depends_on=["va"]), _task("gb", "gate", ["VAL-B"], depends_on=["vb"]), ])) diff --git a/zenith/tests/test_server.py b/zenith/tests/test_server.py index 4b5533c..437b689 100644 --- a/zenith/tests/test_server.py +++ b/zenith/tests/test_server.py @@ -178,12 +178,12 @@ async def test_submit_terminal_review_writes_file(tmp_path: Path, monkeypatch) - @pytest.mark.asyncio async def test_orchestrator_end_to_end_in_process( config: HarnessConfig, workspace: Path -) -> None: + ) -> None: def responder(req): - if req.node.type == "work": - return WorkHandoff(node_id=req.node.id, done=True, report="ok") + if req.task.type == "work": + return WorkHandoff(node_id=req.task.id, done=True, report="ok") return ValidateHandoff( - node_id=req.node.id, + node_id=req.task.id, done=True, report="audited", items=[ValidationItem(item_id="VAL-001", passed=True)], @@ -206,8 +206,8 @@ def responder(req): (contract_dir / "VAL-001.md").write_text("# VAL-001\n") task_list_dict = { "tasks": [ - {"id": "w1", "type": "work", "body": "do", "targets": ["VAL-001"], "skill": "s", "depends_on": []}, - {"id": "v1", "type": "validate", "body": "audit", "targets": ["VAL-001"], "skill": "aud", "depends_on": ["w1"]}, + {"id": "w1", "type": "work", "body": "do", "targets": ["VAL-001"], "skill": "engineering-mission-playbook", "depends_on": []}, + {"id": "v1", "type": "validate", "body": "audit", "targets": ["VAL-001"], "skill": "scrutiny-validator", "depends_on": ["w1"]}, {"id": "g1", "type": "gate", "body": "", "targets": ["VAL-001"], "skill": None, "depends_on": ["v1"]}, ], } @@ -281,8 +281,8 @@ async def test_advance_project_tolerates_asyncio_run_dispatcher( (contract_dir / "VAL-001.md").write_text("# VAL-001\n") task_list_dict = { "tasks": [ - {"id": "w1", "type": "work", "body": "do", "targets": ["VAL-001"], "skill": "s", "depends_on": []}, - {"id": "v1", "type": "validate", "body": "audit", "targets": ["VAL-001"], "skill": "aud", "depends_on": ["w1"]}, + {"id": "w1", "type": "work", "body": "do", "targets": ["VAL-001"], "skill": "engineering-mission-playbook", "depends_on": []}, + {"id": "v1", "type": "validate", "body": "audit", "targets": ["VAL-001"], "skill": "scrutiny-validator", "depends_on": ["w1"]}, {"id": "g1", "type": "gate", "body": "", "targets": ["VAL-001"], "skill": None, "depends_on": ["v1"]}, ], } diff --git a/zenith/tests/test_smoke_real_acp.py b/zenith/tests/test_smoke_real_acp.py index 66b65b6..6d38c79 100644 --- a/zenith/tests/test_smoke_real_acp.py +++ b/zenith/tests/test_smoke_real_acp.py @@ -141,14 +141,13 @@ def _build_task_list() -> TaskList: ]) -def _write_contract(workspace: Path, mission_id: str) -> None: - d = workspace / ".zenith" / "missions" / mission_id / "contract" - d.mkdir(parents=True, exist_ok=True) +def _write_contract(store: ProjectStore, pid: str, mission_id: str) -> None: + d = store.ensure_contract_dir(pid, mission_id) (d / "VAL-HELLO-001.md").write_text(CONTRACT_BODY) -def _write_worker_skill(workspace: Path) -> None: - d = workspace / ".zenith" / "skills" / "hello-file-worker" +def _write_worker_skill(store: ProjectStore, pid: str) -> None: + d = store.zenith_dir(pid) / "skills" / "hello-file-worker" d.mkdir(parents=True, exist_ok=True) (d / "SKILL.md").write_text( """--- @@ -253,12 +252,12 @@ def test_smoke_hello_mission( # 1) start_project start_env = controller.start_project("smoke test brief", str(workspace)) - pid = ProjectStore(config).list_projects()[0].id + pid = controller.store.list_projects()[0].id assert start_env.state.state == "mission_planning" # 2) contract + submit_plan - _write_contract(workspace, "mission-001") - _write_worker_skill(workspace) + _write_contract(controller.store, pid, "mission-001") + _write_worker_skill(controller.store, pid) plan_env = controller.submit_plan(pid, _build_task_list()) assert plan_env.state.state == "mission_running" @@ -266,10 +265,12 @@ def test_smoke_hello_mission( deadline = time.monotonic() + _TIMEOUT_S state_history: list[str] = [] - # Drive: w1 → v1 → g1 → terminal review. + # Drive: w1 → v1 → g1, acknowledge the gate checkpoint, then request + # closure via end_mission. # Each `advance_project` call returns at the next attention/terminal/idle. # We expect: first call → gate_checkpoint; we say continue; - # second call → Done (via clean terminal reviewer). + # second call → mission_running with no runnable task work; + # end_mission → Done (via clean terminal reviewer). advance_env = controller.advance_project(pid) state_history.append(advance_env.state.state) assert ( @@ -289,7 +290,13 @@ def test_smoke_hello_mission( if time.monotonic() > deadline: pytest.fail(f"smoke test exceeded {_TIMEOUT_S}s before terminal review") - final = controller.advance_project(pid) + post_gate = controller.advance_project(pid) + state_history.append(post_gate.state.state) + assert post_gate.state.state == "mission_running", ( + f"expected mission_running after gate acknowledgement, got {post_gate.state.state}; " + f"history={state_history}" + ) + final = controller.end_mission(pid) state_history.append(final.state.state) assert final.state.state == "done", ( f"expected done after terminal review, got {final.state.state}; " @@ -335,7 +342,7 @@ def test_smoke_hello_mission( assert item_passed, f"VAL-HELLO-001 not marked passed: items={val_handoff.items}" # 7) Closeout written - closeout = workspace / ".zenith" / "missions" / "mission-001" / "closeout.md" + closeout = controller.store.mission_dir(pid, "mission-001") / "closeout.md" assert closeout.exists(), f"missing closeout at {closeout}" @@ -377,8 +384,9 @@ def _run_with_real_terminal_reviewer( controller = ProjectController(config, dispatcher, reviewer) controller.start_project("smoke test brief", str(workspace)) - pid = ProjectStore(config).list_projects()[0].id - _write_contract(workspace, "mission-001") + pid = controller.store.list_projects()[0].id + _write_contract(controller.store, pid, "mission-001") + _write_worker_skill(controller.store, pid) controller.submit_plan(pid, _build_task_list()) deadline = time.monotonic() + _TIMEOUT_S @@ -391,14 +399,16 @@ def _run_with_real_terminal_reviewer( ) if time.monotonic() > deadline: pytest.fail("smoke test ran out of budget before terminal review") - final = controller.advance_project(pid) + post_gate = controller.advance_project(pid) + assert post_gate.state.state == "mission_running" + final = controller.end_mission(pid) # Either Done (clean review) or attention_needed (reviewer found gaps). # We accept both — the goal is to confirm the L3 ACP wiring works. assert final.state.state in ("done", "attention_needed"), ( f"unexpected post-terminal state: {final.state.state}" ) - review_dir = workspace / ".zenith" / "missions" / "mission-001" / "terminal-reviews" + review_dir = controller.store.terminal_reviews_dir(pid, "mission-001") assert review_dir.is_dir() and any(review_dir.iterdir()), ( f"terminal reviewer did not write terminal review at {review_dir}" ) diff --git a/zenith/tests/test_terminal_review.py b/zenith/tests/test_terminal_review.py index 4c19166..abf221d 100644 --- a/zenith/tests/test_terminal_review.py +++ b/zenith/tests/test_terminal_review.py @@ -49,7 +49,11 @@ def config(harness_home: Path) -> HarnessConfig: def _task(tid: str, ttype: str, targets: list[str], skill: str | None = None, depends_on: list[str] | None = None) -> Task: if skill is None and ttype != "gate": - skill = "s" + skill = ( + "scrutiny-validator" + if ttype == "validate" + else "engineering-mission-playbook" + ) return Task( id=tid, type=ttype, # type: ignore[arg-type] @@ -63,7 +67,7 @@ def _task(tid: str, ttype: str, targets: list[str], skill: str | None = None, def _simple_tl() -> TaskList: return TaskList(tasks=[ _task("w1", "work", ["VAL-001"]), - _task("v1", "validate", ["VAL-001"], skill="aud", depends_on=["w1"]), + _task("v1", "validate", ["VAL-001"], depends_on=["w1"]), _task("g1", "gate", ["VAL-001"], depends_on=["v1"]), ])